From e10b034f6ab8cfe6126aae125eede7bba8ffddd5 Mon Sep 17 00:00:00 2001 From: azjezz Date: Sun, 10 May 2026 17:50:37 +0100 Subject: [PATCH 01/30] Bound-Erased Generic Types Implementation Signed-off-by: azjezz --- NEWS | 8 + UPGRADING | 51 + UPGRADING.INTERNALS | 48 + Zend/Optimizer/optimize_func_calls.c | 47 + Zend/Optimizer/zend_optimizer.c | 33 + .../declaration/default_satisfies_bound.phpt | 18 + .../default_violates_bound_class.phpt | 9 + .../default_violates_bound_function.phpt | 9 + .../default_violates_bound_interface.phpt | 9 + .../default_violates_bound_trait.phpt | 9 + .../default_with_intersection_bound.phpt | 8 + .../declaration/default_with_union_bound.phpt | 8 + .../error_renders_default_args.phpt | 9 + .../declaration/no_leak_dnf_bound.phpt | 17 + .../no_leak_intersection_bound.phpt | 15 + .../declaration/no_leak_union_bound.phpt | 15 + .../recursive_bounds/chain_forward.phpt | 11 + .../default_backward_ref_allowed.phpt | 11 + .../default_forward_ref_rejected.phpt | 9 + .../direct_self_ref_still_rejected.phpt | 8 + .../extends_with_mutual_bounds.phpt | 17 + .../forward_bound_with_class_bound.phpt | 18 + .../forward_in_generic_arg.phpt | 10 + .../recursive_bounds/forward_simple.phpt | 11 + .../recursive_bounds/four_way_cycle.phpt | 10 + .../recursive_bounds/hammer_combined.phpt | 34 + .../interface_with_mutual_bounds.phpt | 25 + .../recursive_bounds/mutual_class.phpt | 10 + .../recursive_bounds/mutual_function.phpt | 10 + .../mutual_intersection_bound.phpt | 11 + .../recursive_bounds/mutual_method.phpt | 12 + .../recursive_bounds/mutual_union_bound.phpt | 10 + .../nested_mutual_in_outer.phpt | 12 + .../outer_scope_param_in_recursive_bound.phpt | 12 + .../redeclare_still_rejected.phpt | 8 + .../runtime_instantiation.phpt | 20 + .../self_ref_through_generic_arg_allowed.phpt | 10 + .../recursive_bounds/triple_cycle.phpt | 10 + .../recursive_bounds/turbofish_satisfied.phpt | 12 + .../recursive_bounds/turbofish_violation.phpt | 15 + .../with_variance_covariant_violation.phpt | 9 + .../with_variance_invariant.phpt | 12 + .../arrow_violation_covariant_in_param.phpt | 8 + ...oload_unresolved_implements_forwarded.phpt | 20 + ...oload_unresolved_implements_violation.phpt | 19 + .../closure_violation_covariant_in_param.phpt | 8 + .../variance/function_happy_path.phpt | 29 + .../variance/function_violation_composed.phpt | 10 + ...ion_violation_contravariant_in_return.phpt | 8 + ...function_violation_covariant_in_param.phpt | 8 + .../function_violation_own_bound.phpt | 10 + .../declaration/variance/happy_path.phpt | 87 + .../method_function_origin_violation.phpt | 10 + .../mixed_class_T_and_function_T.phpt | 13 + .../variance/parent_keyword_ok.phpt | 16 + .../variance/parent_keyword_violation.phpt | 13 + .../declaration/variance/rfc_example.phpt | 45 + .../declaration/variance/self_keyword_ok.phpt | 14 + ...ord_through_invariant_class_violation.phpt | 10 + .../variance/self_keyword_violation.phpt | 10 + .../variance/self_ref_class_name_ok.phpt | 18 + .../self_ref_class_name_violation.phpt | 10 + .../variance/static_keyword_ok.phpt | 14 + ...atic_method_function_origin_violation.phpt | 10 + .../violation_contravariant_in_get_hook.phpt | 15 + ...on_contravariant_in_readonly_property.phpt | 11 + .../violation_contravariant_in_return.phpt | 10 + ...iolation_contravariant_in_rw_property.phpt | 10 + ...riant_through_covariant_arg_in_return.phpt | 12 + ...violation_covariant_in_byref_get_hook.phpt | 14 + .../violation_covariant_in_default.phpt | 10 + .../violation_covariant_in_own_bound.phpt | 10 + .../violation_covariant_in_param.phpt | 10 + .../violation_covariant_in_rw_property.phpt | 10 + .../violation_covariant_in_set_hook.phpt | 15 + .../violation_covariant_in_sibling_bound.phpt | 10 + ...t_through_contravariant_arg_in_return.phpt | 12 + ...ation_covariant_through_invariant_arg.phpt | 12 + .../violation_extends_contravariant_arg.phpt | 10 + .../violation_extends_invariant_arg.phpt | 10 + .../violation_implements_invariant_arg.phpt | 10 + .../violation_line_points_at_method.phpt | 16 + ...violation_method_T_bound_uses_class_T.phpt | 12 + .../violation_use_trait_invariant_arg.phpt | 14 + .../generics/erasure/arrow_fn_erasure.phpt | 10 + Zend/tests/generics/erasure/bound_object.phpt | 12 + .../generics/erasure/bound_to_class.phpt | 13 + .../generics/erasure/closure_erasure.phpt | 10 + .../generics/erasure/composite_bound.phpt | 11 + .../erasure/erased_signature_match.phpt | 20 + .../erasure/extends_args_discarded.phpt | 13 + .../generics/erasure/generic_class_param.phpt | 13 + .../erasure/get_class_returns_erased.phpt | 10 + .../erasure/instanceof_args_discarded.phpt | 14 + .../erasure/intersection_t_bound_class.phpt | 20 + Zend/tests/generics/erasure/method_bound.phpt | 13 + .../erasure/method_inheritance_erased.phpt | 19 + .../generics/erasure/named_args_stripped.phpt | 13 + .../erasure/property_type_erased.phpt | 16 + .../generics/erasure/runtime_bound_check.phpt | 21 + .../generics/erasure/runtime_unbounded.phpt | 20 + .../erasure/t_in_nullable_and_union.phpt | 72 + .../erasure/turbofish_no_runtime_effect.phpt | 13 + .../generics/erasure/unbounded_to_mixed.phpt | 12 + .../generics/erasure/var_dump_class_name.phpt | 14 + .../errors/anonymous_class_no_params.phpt | 12 + .../errors/closure_shadows_function.phpt | 10 + .../generics/errors/duplicate_in_method.phpt | 8 + .../generics/errors/duplicate_type_param.phpt | 8 + .../errors/duplicate_type_param_function.phpt | 8 + .../tests/generics/errors/empty_arg_list.phpt | 12 + .../generics/errors/empty_param_list.phpt | 12 + .../generics/errors/forward_ref_at_self.phpt | 8 + .../errors/forward_reference_in_default.phpt | 8 + .../errors/intersection_t_bound_scalar.phpt | 9 + .../errors/intersection_t_unbounded.phpt | 9 + .../errors/method_shadows_class_param.phpt | 10 + .../generics/errors/missing_close_angle.phpt | 12 + .../errors/missing_param_after_comma.phpt | 12 + .../errors/optional_only_at_tail_ok.phpt | 15 + .../generics/errors/recursive_bound_ok.phpt | 10 + .../errors/required_after_optional.phpt | 8 + .../required_after_optional_function.phpt | 8 + .../required_after_optional_three_params.phpt | 8 + .../errors/runtime_bound_violation.phpt | 16 + .../runtime_unbound_accepts_anything.phpt | 19 + .../generics/errors/self_ref_in_default.phpt | 8 + .../errors/self_ref_in_intersection.phpt | 9 + .../generics/errors/self_ref_in_union.phpt | 8 + .../errors/turbofish_no_call_target.phpt | 12 + .../generics/errors/turbofish_with_space.phpt | 13 + .../errors/type_args_in_class_const.phpt | 13 + .../errors/type_args_in_static_call.phpt | 13 + .../generics/errors/type_param_in_catch.phpt | 10 + .../errors/type_param_in_class_const.phpt | 10 + .../errors/type_param_in_instanceof.phpt | 10 + .../errors/type_param_in_new_expression.phpt | 10 + .../errors/type_param_in_static_call.phpt | 10 + .../errors/type_param_in_static_method.phpt | 10 + .../type_param_in_static_method_bound.phpt | 10 + .../type_param_in_static_method_default.phpt | 10 + .../type_param_in_static_method_in_trait.phpt | 10 + ...e_param_in_static_method_intersection.phpt | 10 + .../type_param_in_static_method_nested.phpt | 14 + .../type_param_in_static_method_nullable.phpt | 10 + .../type_param_in_static_method_return.phpt | 10 + .../type_param_in_static_method_variadic.phpt | 10 + .../errors/type_param_in_static_property.phpt | 10 + ...ype_param_in_static_property_in_trait.phpt | 10 + .../type_param_in_static_property_nested.phpt | 14 + .../arity/extends_no_args_required.phpt | 9 + .../arity/extends_non_generic.phpt | 9 + .../arity/extends_over_default_max.phpt | 9 + .../inheritance/arity/extends_too_few.phpt | 9 + .../inheritance/arity/extends_too_many.phpt | 9 + .../arity/implements_no_args_required.phpt | 9 + .../inheritance/arity/implements_too_few.phpt | 9 + .../arity/interface_extends_arity.phpt | 9 + .../arity/no_args_with_defaults.phpt | 16 + .../arity/use_trait_no_args_required.phpt | 9 + .../inheritance/arity/use_trait_too_many.phpt | 9 + .../inheritance/arity/with_defaults.phpt | 19 + .../bounds/bound_on_bound_pass.phpt | 18 + .../bounds/bound_on_bound_too_loose.phpt | 12 + .../bounds/bound_on_bound_unbounded_arg.phpt | 10 + .../bounds/error_renders_args.phpt | 12 + .../bounds/extends_concrete_satisfies.phpt | 13 + .../inheritance/bounds/extends_violates.phpt | 11 + .../bounds/implements_violates.phpt | 11 + .../interface_extends_interface_violates.phpt | 11 + .../bounds/use_trait_violates.phpt | 11 + .../conflicting_parent_and_interface.phpt | 14 + .../conflicting_via_two_interfaces.phpt | 16 + .../diamond/forwarding_t_consistent.phpt | 12 + .../diamond/forwarding_t_vs_concrete.phpt | 14 + ..._diamond_class_impl_covariant_missing.phpt | 17 + .../interface_diamond_class_impl_missing.phpt | 13 + .../diamond/interface_diamond_variance.phpt | 121 + ...e_diamond_variance_invariant_rejected.phpt | 10 + ...riant_class_diamond_bad_impl_rejected.phpt | 20 + ..._class_diamond_with_use_site_variance.phpt | 34 + ...t_interface_diamond_bad_impl_rejected.phpt | 19 + ...nvariant_interface_diamond_class_impl.phpt | 31 + .../invariant_interface_diamond_merge.phpt | 32 + ...iant_interface_diamond_still_rejected.phpt | 11 + .../invariant_tapper_class_diamond.phpt | 52 + .../diamond/matching_args_via_two_paths.phpt | 13 + .../diamond/nullable_intersection_merge.phpt | 28 + ..._intersection_missing_branch_rejected.phpt | 16 + .../diamond/nullable_union_merge.phpt | 18 + .../nullable_union_missing_null_rejected.phpt | 13 + .../parent_supplies_interface_matches.phpt | 12 + .../property_hook_get_only_diamond.phpt | 32 + .../property_hook_get_only_diamond_bad.phpt | 18 + .../diamond/reflection_merged_signatures.phpt | 50 + ...r_intersection_class_diamond_rejected.phpt | 16 + ...calar_intersection_covariant_rejected.phpt | 14 + ...ersection_invariant_use_site_rejected.phpt | 14 + .../scalar_intersection_trait_rejected.phpt | 14 + .../diamond/scalar_union_param_accepted.phpt | 17 + .../diamond/trait_diamond_compiles.phpt | 26 + .../diamond/trait_diamond_covariant.phpt | 29 + .../trait_diamond_invariant_use_site.phpt | 32 + .../diamond/trait_diamond_mixed_methods.phpt | 28 + .../trait_diamond_multi_param_mixed.phpt | 35 + .../diamond/trait_diamond_three_uses.phpt | 26 + .../diamond/trait_diamond_variadic.phpt | 28 + .../diamond/variadic_bad_impl_rejected.phpt | 11 + .../variadic_contravariant_diamond.phpt | 39 + .../diamond/variance_positive_all_paths.phpt | 234 ++ .../inheritance/lsp/abstract_class_chain.phpt | 20 + .../inheritance/lsp/abstract_method.phpt | 17 + .../lsp/basic_param_and_return.phpt | 29 + .../inheritance/lsp/bounded_param.phpt | 19 + .../lsp/by_reference_substituted.phpt | 18 + .../inheritance/lsp/contravariant_param.phpt | 21 + .../inheritance/lsp/covariant_return.phpt | 19 + .../inheritance/lsp/default_param.phpt | 16 + .../lsp/diamond_via_interfaces.phpt | 19 + .../lsp/extends_and_implements_generic.phpt | 23 + .../lsp/generic_class_extends_generic.phpt | 18 + .../lsp/method_param_separate_scope.phpt | 20 + .../inheritance/lsp/method_substituted.phpt | 16 + .../inheritance/lsp/multi_interface.phpt | 25 + .../generics/inheritance/lsp/multi_param.phpt | 23 + .../lsp/non_generic_inherits_unrelated.phpt | 16 + .../lsp/nullable_T_chain_subst.phpt | 18 + .../lsp/nullable_T_param_subst.phpt | 14 + .../lsp/nullable_T_return_subst.phpt | 16 + .../lsp/optional_arg_substituted.phpt | 27 + .../generics/inheritance/lsp/parent_call.phpt | 22 + .../inheritance/lsp/reordered_params.phpt | 32 + .../lsp/transitive_three_level.phpt | 18 + .../lsp/transitive_through_class.phpt | 19 + .../inheritance/lsp/transitive_two_level.phpt | 20 + .../inheritance/lsp/variadic_substituted.phpt | 24 + ...ation_extends_return_sibling_of_bound.phpt | 18 + .../violation_extends_return_transitive.phpt | 16 + .../violation_extends_return_unbounded.phpt | 14 + .../lsp/violation_nullable_T_message.phpt | 14 + .../lsp/violation_param_too_specific.phpt | 17 + .../lsp/violation_return_too_general.phpt | 17 + .../method/inherited_concrete_unchanged.phpt | 16 + .../method/inherited_from_interface.phpt | 15 + ...ed_method_with_named_with_args_return.phpt | 20 + .../inherited_signature_substituted.phpt | 21 + .../inherited_signature_transitive.phpt | 17 + .../inherited_variadic_substituted.phpt | 15 + .../property/extends_concrete_arg.phpt | 27 + .../extends_concrete_property_unchanged.phpt | 17 + .../property/extends_default_arg.phpt | 23 + .../property/extends_get_and_set_hook.phpt | 27 + .../property/extends_get_hook.phpt | 24 + .../property/extends_get_hook_transitive.phpt | 25 + .../property/extends_set_hook.phpt | 32 + .../property/extends_transitive.phpt | 24 + .../extends_two_classes_distinct_types.phpt | 25 + .../self_and_static_with_generics.phpt | 36 + .../arg_list_return_type_128_rejected.phpt | 16 + .../limit/param_list_class_128_rejected.phpt | 15 + .../turbofish_function_call_128_rejected.phpt | 16 + ...pace_relative_extends_implements_uses.phpt | 64 + ...losure_returned_visible_in_reflection.phpt | 13 + .../generics/scoping/extends_clause.phpt | 11 + .../file_scope_class_does_not_shadow.phpt | 11 + .../scoping/fq_name_bypasses_type_param.phpt | 15 + .../generics/scoping/implements_clause.phpt | 11 + ...r_class_shadowing_is_case_insensitive.phpt | 15 + .../inner_class_shadows_in_instanceof.phpt | 14 + .../inner_class_shadows_type_param.phpt | 15 + .../scoping/left_to_right_param_bounds.phpt | 12 + .../scoping/method_introduces_own.phpt | 18 + .../scoping/method_param_does_not_leak.phpt | 20 + .../generics/scoping/method_param_type.phpt | 13 + .../generics/scoping/method_return_type.phpt | 13 + .../scoping/nested_anon_class_captures.phpt | 21 + .../scoping/nested_arrow_captures.phpt | 15 + .../scoping/nested_closure_captures.phpt | 16 + .../tests/generics/scoping/property_type.phpt | 12 + .../generics/scoping/trait_use_clause.phpt | 17 + Zend/tests/generics/syntax/args_dnf.phpt | 15 + .../generics/syntax/args_intersection.phpt | 14 + Zend/tests/generics/syntax/args_union.phpt | 12 + Zend/tests/generics/syntax/arrow_fn_decl.phpt | 12 + .../syntax/bound_and_default_with_args.phpt | 20 + Zend/tests/generics/syntax/bound_dnf.phpt | 13 + .../generics/syntax/bound_intersection.phpt | 12 + .../generics/syntax/bound_single_class.phpt | 13 + Zend/tests/generics/syntax/bound_union.phpt | 11 + .../syntax/builtin_self_parent_static.phpt | 20 + .../generics/syntax/catch_with_args.phpt | 13 + .../syntax/class_multiple_params.phpt | 14 + .../generics/syntax/class_single_param.phpt | 14 + Zend/tests/generics/syntax/closure_decl.phpt | 12 + Zend/tests/generics/syntax/default_value.phpt | 12 + .../syntax/default_with_nested_args.phpt | 11 + .../generics/syntax/extends_with_args.phpt | 10 + Zend/tests/generics/syntax/function_decl.phpt | 12 + .../generics/syntax/implements_with_args.phpt | 11 + .../generics/syntax/instanceof_with_args.phpt | 15 + .../tests/generics/syntax/interface_decl.phpt | 12 + Zend/tests/generics/syntax/method_decl.phpt | 14 + .../generics/syntax/named_type_args.phpt | 16 + .../generics/syntax/nested_three_levels.phpt | 21 + .../generics/syntax/nested_two_levels.phpt | 17 + .../generics/syntax/trailing_comma_args.phpt | 11 + .../syntax/trailing_comma_params.phpt | 12 + Zend/tests/generics/syntax/trait_decl.phpt | 12 + .../tests/generics/syntax/trait_use_args.phpt | 15 + .../syntax/turbofish_complex_args.phpt | 11 + Zend/tests/generics/syntax/turbofish_fcc.phpt | 24 + .../syntax/turbofish_function_call.phpt | 13 + .../syntax/turbofish_method_call.phpt | 17 + Zend/tests/generics/syntax/turbofish_new.phpt | 32 + .../syntax/turbofish_static_call.phpt | 13 + .../syntax/variance_bound_default.phpt | 11 + .../syntax/variance_contravariant.phpt | 12 + .../generics/syntax/variance_covariant.phpt | 12 + .../syntax/variance_invariant_default.phpt | 12 + .../traits/abstract_method_implemented.phpt | 17 + .../traits/abstract_method_two_classes.phpt | 24 + .../traits/abstract_method_violates.phpt | 15 + Zend/tests/generics/traits/basic_trait.phpt | 18 + .../traits/method_concrete_unchanged.phpt | 25 + .../generics/traits/method_default_arg.phpt | 20 + .../traits/method_signature_substituted.phpt | 25 + .../traits/method_variadic_substituted.phpt | 19 + .../generics/traits/multiple_traits.phpt | 22 + ...roperty_no_substitution_when_concrete.phpt | 17 + .../property_substituted_two_classes.phpt | 27 + .../property_substituted_with_class_arg.phpt | 29 + .../generics/traits/trait_constant_type.phpt | 18 + .../traits/trait_get_generic_params.phpt | 14 + .../generics/traits/trait_isgeneric.phpt | 12 + .../traits/trait_method_isgeneric.phpt | 17 + .../generics/traits/trait_method_runtime.phpt | 26 + .../traits/trait_property_substituted.phpt | 32 + .../generics/traits/trait_property_type.phpt | 16 + .../traits/trait_use_args_compile.phpt | 17 + .../traits/trait_with_method_param.phpt | 17 + .../turbofish/arity/attribute_arity.phpt | 38 + .../turbofish/arity/below_required.phpt | 13 + .../turbofish/arity/function_too_few.phpt | 13 + .../turbofish/arity/function_too_many.phpt | 13 + .../turbofish/arity/method_arity.phpt | 24 + .../generics/turbofish/arity/new_arity.phpt | 28 + .../turbofish/arity/non_generic_callee.phpt | 13 + .../turbofish/arity/with_defaults.phpt | 27 + .../turbofish/bounds/attribute_satisfies.phpt | 22 + .../bounds/attribute_under_opcache.phpt | 28 + .../turbofish/bounds/attribute_violates.phpt | 24 + .../turbofish/bounds/error_renders_args.phpt | 16 + .../bounds/function_call_violates.phpt | 15 + .../bounds/method_call_violates.phpt | 25 + .../bounds/multi_param_first_violates.phpt | 17 + .../bounds/multi_param_second_violates.phpt | 17 + .../turbofish/bounds/new_satisfies.phpt | 14 + .../turbofish/bounds/new_violates.phpt | 15 + .../turbofish/bounds/no_bound_no_check.phpt | 15 + .../generics/turbofish/composite_args.phpt | 13 + .../generics/turbofish/fcc_function.phpt | 10 + Zend/tests/generics/turbofish/fcc_method.phpt | 10 + .../generics/turbofish/fcc_static_method.phpt | 10 + .../generics/turbofish/function_call.phpt | 9 + .../turbofish/function_call_multi.phpt | 14 + Zend/tests/generics/turbofish/in_chain.phpt | 13 + .../turbofish/instance_method_call.phpt | 12 + .../turbofish/instanceof_with_args.phpt | 15 + .../generics/turbofish/many_type_args.phpt | 13 + .../tests/generics/turbofish/nested_args.phpt | 15 + .../generics/turbofish/new_with_args.phpt | 13 + .../generics/turbofish/no_runtime_effect.phpt | 15 + .../turbofish/nullsafe_method_call.phpt | 15 + .../turbofish/static_method_call.phpt | 11 + .../turbofish/turbofish_in_attribute.phpt | 39 + .../generics/turbofish/with_named_args.phpt | 11 + .../tests/generics/turbofish/with_spread.phpt | 10 + Zend/zend.h | 3 + Zend/zend_ast.c | 120 +- Zend/zend_ast.h | 27 +- Zend/zend_attributes.c | 7 + Zend/zend_attributes.h | 3 + Zend/zend_compile.c | 1048 +++++++- Zend/zend_compile.h | 84 +- Zend/zend_globals.h | 19 + Zend/zend_inheritance.c | 2342 ++++++++++++++++- Zend/zend_inheritance.h | 34 +- Zend/zend_language_parser.y | 227 +- Zend/zend_language_scanner.l | 4 + Zend/zend_opcode.c | 187 ++ Zend/zend_types.h | 46 +- Zend/zend_vm_def.h | 35 + Zend/zend_vm_execute.h | 349 ++- Zend/zend_vm_handlers.h | 930 +++---- Zend/zend_vm_opcodes.c | 6 +- Zend/zend_vm_opcodes.h | 3 +- docs/source/core/generics.rst | 241 ++ docs/source/index.rst | 1 + ext/opcache/zend_file_cache.c | 247 ++ ext/opcache/zend_persist.c | 294 ++- ext/opcache/zend_persist_calc.c | 200 ++ ext/reflection/php_reflection.c | 1041 +++++++- ext/reflection/php_reflection.stub.php | 110 + ext/reflection/php_reflection_arginfo.h | 163 +- ext/reflection/php_reflection_decl.h | 14 +- .../tests/ReflectionClass_toString_001.phpt | 39 +- .../ReflectionExtension_getClasses_basic.phpt | 17 +- .../generics/ancestor_args_composite.phpt | 19 + .../ancestor_args_duplicate_bindings.phpt | 64 + .../tests/generics/ancestor_args_nested.phpt | 41 + .../ancestor_args_nested_transitive.phpt | 49 + .../ancestor_args_no_generics_class.phpt | 47 + .../generics/ancestor_args_parent_class.phpt | 39 + .../ancestor_args_parent_interface.phpt | 59 + .../ancestor_args_transitive_paths.phpt | 77 + .../ancestor_args_type_param_refs.phpt | 26 + .../generics/ancestor_args_used_trait.phpt | 46 + .../generics/arrow_fn_get_generic_params.phpt | 14 + .../tests/generics/class_isgeneric_false.phpt | 9 + .../tests/generics/class_isgeneric_true.phpt | 9 + .../generics/closure_get_generic_params.phpt | 16 + .../tests/generics/composite_bound.phpt | 12 + .../tests/generics/composite_type_arg.phpt | 14 + .../tests/generics/function_isgeneric.phpt | 12 + .../get_generic_parameters_count.phpt | 16 + .../get_generic_parameters_empty.phpt | 14 + .../generics/method_get_generic_params.phpt | 16 + .../tests/generics/method_isgeneric.phpt | 14 + .../generics/named_type_get_name_erased.phpt | 13 + .../tests/generics/named_type_has_args.phpt | 24 + .../tests/generics/named_type_no_args.phpt | 12 + .../tests/generics/nested_args.phpt | 17 + .../parameter_constructor_throws.phpt | 14 + .../tests/generics/parameter_get_bound.phpt | 18 + .../parameter_get_declaring_class.phpt | 13 + .../parameter_get_declaring_function.phpt | 13 + .../generics/parameter_get_position.phpt | 14 + .../generics/parameter_get_variance.phpt | 13 + .../tests/generics/parameter_has_bound.phpt | 12 + .../tests/generics/parameter_has_default.phpt | 20 + .../tests/generics/parameter_to_string.phpt | 14 + .../tests/generics/property_type_args.phpt | 17 + .../type_parameter_ref_get_param.phpt | 19 + .../generics/type_parameter_reference.phpt | 21 + .../tests/generics/variance_enum.phpt | 18 + ext/tokenizer/tokenizer_data.c | 1 + ext/tokenizer/tokenizer_data.stub.php | 5 + ext/tokenizer/tokenizer_data_arginfo.h | 3 +- 448 files changed, 14372 insertions(+), 851 deletions(-) create mode 100644 Zend/tests/generics/declaration/default_satisfies_bound.phpt create mode 100644 Zend/tests/generics/declaration/default_violates_bound_class.phpt create mode 100644 Zend/tests/generics/declaration/default_violates_bound_function.phpt create mode 100644 Zend/tests/generics/declaration/default_violates_bound_interface.phpt create mode 100644 Zend/tests/generics/declaration/default_violates_bound_trait.phpt create mode 100644 Zend/tests/generics/declaration/default_with_intersection_bound.phpt create mode 100644 Zend/tests/generics/declaration/default_with_union_bound.phpt create mode 100644 Zend/tests/generics/declaration/error_renders_default_args.phpt create mode 100644 Zend/tests/generics/declaration/no_leak_dnf_bound.phpt create mode 100644 Zend/tests/generics/declaration/no_leak_intersection_bound.phpt create mode 100644 Zend/tests/generics/declaration/no_leak_union_bound.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/chain_forward.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/default_backward_ref_allowed.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/default_forward_ref_rejected.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/direct_self_ref_still_rejected.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/forward_bound_with_class_bound.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/forward_in_generic_arg.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/forward_simple.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/four_way_cycle.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/hammer_combined.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/interface_with_mutual_bounds.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/mutual_class.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/mutual_function.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/mutual_intersection_bound.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/mutual_method.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/mutual_union_bound.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/nested_mutual_in_outer.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/outer_scope_param_in_recursive_bound.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/redeclare_still_rejected.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/triple_cycle.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/turbofish_satisfied.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/turbofish_violation.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/with_variance_covariant_violation.phpt create mode 100644 Zend/tests/generics/declaration/recursive_bounds/with_variance_invariant.phpt create mode 100644 Zend/tests/generics/declaration/variance/arrow_violation_covariant_in_param.phpt create mode 100644 Zend/tests/generics/declaration/variance/autoload_unresolved_implements_forwarded.phpt create mode 100644 Zend/tests/generics/declaration/variance/autoload_unresolved_implements_violation.phpt create mode 100644 Zend/tests/generics/declaration/variance/closure_violation_covariant_in_param.phpt create mode 100644 Zend/tests/generics/declaration/variance/function_happy_path.phpt create mode 100644 Zend/tests/generics/declaration/variance/function_violation_composed.phpt create mode 100644 Zend/tests/generics/declaration/variance/function_violation_contravariant_in_return.phpt create mode 100644 Zend/tests/generics/declaration/variance/function_violation_covariant_in_param.phpt create mode 100644 Zend/tests/generics/declaration/variance/function_violation_own_bound.phpt create mode 100644 Zend/tests/generics/declaration/variance/happy_path.phpt create mode 100644 Zend/tests/generics/declaration/variance/method_function_origin_violation.phpt create mode 100644 Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt create mode 100644 Zend/tests/generics/declaration/variance/parent_keyword_ok.phpt create mode 100644 Zend/tests/generics/declaration/variance/parent_keyword_violation.phpt create mode 100644 Zend/tests/generics/declaration/variance/rfc_example.phpt create mode 100644 Zend/tests/generics/declaration/variance/self_keyword_ok.phpt create mode 100644 Zend/tests/generics/declaration/variance/self_keyword_through_invariant_class_violation.phpt create mode 100644 Zend/tests/generics/declaration/variance/self_keyword_violation.phpt create mode 100644 Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt create mode 100644 Zend/tests/generics/declaration/variance/self_ref_class_name_violation.phpt create mode 100644 Zend/tests/generics/declaration/variance/static_keyword_ok.phpt create mode 100644 Zend/tests/generics/declaration/variance/static_method_function_origin_violation.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_contravariant_in_get_hook.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_contravariant_in_readonly_property.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_contravariant_in_return.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_contravariant_in_rw_property.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_contravariant_through_covariant_arg_in_return.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_in_byref_get_hook.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_in_default.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_in_own_bound.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_in_param.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_in_rw_property.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_in_set_hook.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_in_sibling_bound.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_through_contravariant_arg_in_return.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_covariant_through_invariant_arg.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_extends_contravariant_arg.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_extends_invariant_arg.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_implements_invariant_arg.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_line_points_at_method.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_method_T_bound_uses_class_T.phpt create mode 100644 Zend/tests/generics/declaration/variance/violation_use_trait_invariant_arg.phpt create mode 100644 Zend/tests/generics/erasure/arrow_fn_erasure.phpt create mode 100644 Zend/tests/generics/erasure/bound_object.phpt create mode 100644 Zend/tests/generics/erasure/bound_to_class.phpt create mode 100644 Zend/tests/generics/erasure/closure_erasure.phpt create mode 100644 Zend/tests/generics/erasure/composite_bound.phpt create mode 100644 Zend/tests/generics/erasure/erased_signature_match.phpt create mode 100644 Zend/tests/generics/erasure/extends_args_discarded.phpt create mode 100644 Zend/tests/generics/erasure/generic_class_param.phpt create mode 100644 Zend/tests/generics/erasure/get_class_returns_erased.phpt create mode 100644 Zend/tests/generics/erasure/instanceof_args_discarded.phpt create mode 100644 Zend/tests/generics/erasure/intersection_t_bound_class.phpt create mode 100644 Zend/tests/generics/erasure/method_bound.phpt create mode 100644 Zend/tests/generics/erasure/method_inheritance_erased.phpt create mode 100644 Zend/tests/generics/erasure/named_args_stripped.phpt create mode 100644 Zend/tests/generics/erasure/property_type_erased.phpt create mode 100644 Zend/tests/generics/erasure/runtime_bound_check.phpt create mode 100644 Zend/tests/generics/erasure/runtime_unbounded.phpt create mode 100644 Zend/tests/generics/erasure/t_in_nullable_and_union.phpt create mode 100644 Zend/tests/generics/erasure/turbofish_no_runtime_effect.phpt create mode 100644 Zend/tests/generics/erasure/unbounded_to_mixed.phpt create mode 100644 Zend/tests/generics/erasure/var_dump_class_name.phpt create mode 100644 Zend/tests/generics/errors/anonymous_class_no_params.phpt create mode 100644 Zend/tests/generics/errors/closure_shadows_function.phpt create mode 100644 Zend/tests/generics/errors/duplicate_in_method.phpt create mode 100644 Zend/tests/generics/errors/duplicate_type_param.phpt create mode 100644 Zend/tests/generics/errors/duplicate_type_param_function.phpt create mode 100644 Zend/tests/generics/errors/empty_arg_list.phpt create mode 100644 Zend/tests/generics/errors/empty_param_list.phpt create mode 100644 Zend/tests/generics/errors/forward_ref_at_self.phpt create mode 100644 Zend/tests/generics/errors/forward_reference_in_default.phpt create mode 100644 Zend/tests/generics/errors/intersection_t_bound_scalar.phpt create mode 100644 Zend/tests/generics/errors/intersection_t_unbounded.phpt create mode 100644 Zend/tests/generics/errors/method_shadows_class_param.phpt create mode 100644 Zend/tests/generics/errors/missing_close_angle.phpt create mode 100644 Zend/tests/generics/errors/missing_param_after_comma.phpt create mode 100644 Zend/tests/generics/errors/optional_only_at_tail_ok.phpt create mode 100644 Zend/tests/generics/errors/recursive_bound_ok.phpt create mode 100644 Zend/tests/generics/errors/required_after_optional.phpt create mode 100644 Zend/tests/generics/errors/required_after_optional_function.phpt create mode 100644 Zend/tests/generics/errors/required_after_optional_three_params.phpt create mode 100644 Zend/tests/generics/errors/runtime_bound_violation.phpt create mode 100644 Zend/tests/generics/errors/runtime_unbound_accepts_anything.phpt create mode 100644 Zend/tests/generics/errors/self_ref_in_default.phpt create mode 100644 Zend/tests/generics/errors/self_ref_in_intersection.phpt create mode 100644 Zend/tests/generics/errors/self_ref_in_union.phpt create mode 100644 Zend/tests/generics/errors/turbofish_no_call_target.phpt create mode 100644 Zend/tests/generics/errors/turbofish_with_space.phpt create mode 100644 Zend/tests/generics/errors/type_args_in_class_const.phpt create mode 100644 Zend/tests/generics/errors/type_args_in_static_call.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_catch.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_class_const.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_instanceof.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_new_expression.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_call.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method_bound.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method_default.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method_in_trait.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method_intersection.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method_nested.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method_nullable.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method_return.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_method_variadic.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_property.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_property_in_trait.phpt create mode 100644 Zend/tests/generics/errors/type_param_in_static_property_nested.phpt create mode 100644 Zend/tests/generics/inheritance/arity/extends_no_args_required.phpt create mode 100644 Zend/tests/generics/inheritance/arity/extends_non_generic.phpt create mode 100644 Zend/tests/generics/inheritance/arity/extends_over_default_max.phpt create mode 100644 Zend/tests/generics/inheritance/arity/extends_too_few.phpt create mode 100644 Zend/tests/generics/inheritance/arity/extends_too_many.phpt create mode 100644 Zend/tests/generics/inheritance/arity/implements_no_args_required.phpt create mode 100644 Zend/tests/generics/inheritance/arity/implements_too_few.phpt create mode 100644 Zend/tests/generics/inheritance/arity/interface_extends_arity.phpt create mode 100644 Zend/tests/generics/inheritance/arity/no_args_with_defaults.phpt create mode 100644 Zend/tests/generics/inheritance/arity/use_trait_no_args_required.phpt create mode 100644 Zend/tests/generics/inheritance/arity/use_trait_too_many.phpt create mode 100644 Zend/tests/generics/inheritance/arity/with_defaults.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/bound_on_bound_pass.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/bound_on_bound_too_loose.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/bound_on_bound_unbounded_arg.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/error_renders_args.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/extends_concrete_satisfies.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/extends_violates.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/implements_violates.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/interface_extends_interface_violates.phpt create mode 100644 Zend/tests/generics/inheritance/bounds/use_trait_violates.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/conflicting_parent_and_interface.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/conflicting_via_two_interfaces.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/forwarding_t_consistent.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_covariant_missing.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_missing.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/interface_diamond_variance.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/interface_diamond_variance_invariant_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/invariant_class_diamond_bad_impl_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/invariant_class_diamond_with_use_site_variance.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_bad_impl_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_class_impl.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_merge.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/invariant_tapper_class_diamond.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/matching_args_via_two_paths.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/nullable_intersection_merge.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/nullable_intersection_missing_branch_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/nullable_union_merge.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/nullable_union_missing_null_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/parent_supplies_interface_matches.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond_bad.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/reflection_merged_signatures.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/scalar_intersection_class_diamond_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/scalar_intersection_covariant_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/scalar_intersection_invariant_use_site_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/scalar_intersection_trait_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/scalar_union_param_accepted.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/trait_diamond_compiles.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/trait_diamond_covariant.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/trait_diamond_invariant_use_site.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/trait_diamond_mixed_methods.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/trait_diamond_multi_param_mixed.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/trait_diamond_three_uses.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/trait_diamond_variadic.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/variadic_bad_impl_rejected.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/variadic_contravariant_diamond.phpt create mode 100644 Zend/tests/generics/inheritance/diamond/variance_positive_all_paths.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/abstract_class_chain.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/abstract_method.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/basic_param_and_return.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/bounded_param.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/by_reference_substituted.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/contravariant_param.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/covariant_return.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/default_param.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/diamond_via_interfaces.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/extends_and_implements_generic.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/generic_class_extends_generic.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/method_param_separate_scope.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/method_substituted.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/multi_interface.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/multi_param.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/non_generic_inherits_unrelated.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/nullable_T_chain_subst.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/nullable_T_param_subst.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/nullable_T_return_subst.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/optional_arg_substituted.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/parent_call.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/reordered_params.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/transitive_three_level.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/transitive_through_class.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/transitive_two_level.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/variadic_substituted.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/violation_extends_return_sibling_of_bound.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/violation_extends_return_transitive.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/violation_extends_return_unbounded.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/violation_nullable_T_message.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/violation_param_too_specific.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/violation_return_too_general.phpt create mode 100644 Zend/tests/generics/inheritance/method/inherited_concrete_unchanged.phpt create mode 100644 Zend/tests/generics/inheritance/method/inherited_from_interface.phpt create mode 100644 Zend/tests/generics/inheritance/method/inherited_method_with_named_with_args_return.phpt create mode 100644 Zend/tests/generics/inheritance/method/inherited_signature_substituted.phpt create mode 100644 Zend/tests/generics/inheritance/method/inherited_signature_transitive.phpt create mode 100644 Zend/tests/generics/inheritance/method/inherited_variadic_substituted.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_concrete_arg.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_concrete_property_unchanged.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_default_arg.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_get_and_set_hook.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_get_hook.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_get_hook_transitive.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_set_hook.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_transitive.phpt create mode 100644 Zend/tests/generics/inheritance/property/extends_two_classes_distinct_types.phpt create mode 100644 Zend/tests/generics/inheritance/self_and_static_with_generics.phpt create mode 100644 Zend/tests/generics/limit/arg_list_return_type_128_rejected.phpt create mode 100644 Zend/tests/generics/limit/param_list_class_128_rejected.phpt create mode 100644 Zend/tests/generics/limit/turbofish_function_call_128_rejected.phpt create mode 100644 Zend/tests/generics/reflection/namespace_relative_extends_implements_uses.phpt create mode 100644 Zend/tests/generics/scoping/closure_returned_visible_in_reflection.phpt create mode 100644 Zend/tests/generics/scoping/extends_clause.phpt create mode 100644 Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt create mode 100644 Zend/tests/generics/scoping/fq_name_bypasses_type_param.phpt create mode 100644 Zend/tests/generics/scoping/implements_clause.phpt create mode 100644 Zend/tests/generics/scoping/inner_class_shadowing_is_case_insensitive.phpt create mode 100644 Zend/tests/generics/scoping/inner_class_shadows_in_instanceof.phpt create mode 100644 Zend/tests/generics/scoping/inner_class_shadows_type_param.phpt create mode 100644 Zend/tests/generics/scoping/left_to_right_param_bounds.phpt create mode 100644 Zend/tests/generics/scoping/method_introduces_own.phpt create mode 100644 Zend/tests/generics/scoping/method_param_does_not_leak.phpt create mode 100644 Zend/tests/generics/scoping/method_param_type.phpt create mode 100644 Zend/tests/generics/scoping/method_return_type.phpt create mode 100644 Zend/tests/generics/scoping/nested_anon_class_captures.phpt create mode 100644 Zend/tests/generics/scoping/nested_arrow_captures.phpt create mode 100644 Zend/tests/generics/scoping/nested_closure_captures.phpt create mode 100644 Zend/tests/generics/scoping/property_type.phpt create mode 100644 Zend/tests/generics/scoping/trait_use_clause.phpt create mode 100644 Zend/tests/generics/syntax/args_dnf.phpt create mode 100644 Zend/tests/generics/syntax/args_intersection.phpt create mode 100644 Zend/tests/generics/syntax/args_union.phpt create mode 100644 Zend/tests/generics/syntax/arrow_fn_decl.phpt create mode 100644 Zend/tests/generics/syntax/bound_and_default_with_args.phpt create mode 100644 Zend/tests/generics/syntax/bound_dnf.phpt create mode 100644 Zend/tests/generics/syntax/bound_intersection.phpt create mode 100644 Zend/tests/generics/syntax/bound_single_class.phpt create mode 100644 Zend/tests/generics/syntax/bound_union.phpt create mode 100644 Zend/tests/generics/syntax/builtin_self_parent_static.phpt create mode 100644 Zend/tests/generics/syntax/catch_with_args.phpt create mode 100644 Zend/tests/generics/syntax/class_multiple_params.phpt create mode 100644 Zend/tests/generics/syntax/class_single_param.phpt create mode 100644 Zend/tests/generics/syntax/closure_decl.phpt create mode 100644 Zend/tests/generics/syntax/default_value.phpt create mode 100644 Zend/tests/generics/syntax/default_with_nested_args.phpt create mode 100644 Zend/tests/generics/syntax/extends_with_args.phpt create mode 100644 Zend/tests/generics/syntax/function_decl.phpt create mode 100644 Zend/tests/generics/syntax/implements_with_args.phpt create mode 100644 Zend/tests/generics/syntax/instanceof_with_args.phpt create mode 100644 Zend/tests/generics/syntax/interface_decl.phpt create mode 100644 Zend/tests/generics/syntax/method_decl.phpt create mode 100644 Zend/tests/generics/syntax/named_type_args.phpt create mode 100644 Zend/tests/generics/syntax/nested_three_levels.phpt create mode 100644 Zend/tests/generics/syntax/nested_two_levels.phpt create mode 100644 Zend/tests/generics/syntax/trailing_comma_args.phpt create mode 100644 Zend/tests/generics/syntax/trailing_comma_params.phpt create mode 100644 Zend/tests/generics/syntax/trait_decl.phpt create mode 100644 Zend/tests/generics/syntax/trait_use_args.phpt create mode 100644 Zend/tests/generics/syntax/turbofish_complex_args.phpt create mode 100644 Zend/tests/generics/syntax/turbofish_fcc.phpt create mode 100644 Zend/tests/generics/syntax/turbofish_function_call.phpt create mode 100644 Zend/tests/generics/syntax/turbofish_method_call.phpt create mode 100644 Zend/tests/generics/syntax/turbofish_new.phpt create mode 100644 Zend/tests/generics/syntax/turbofish_static_call.phpt create mode 100644 Zend/tests/generics/syntax/variance_bound_default.phpt create mode 100644 Zend/tests/generics/syntax/variance_contravariant.phpt create mode 100644 Zend/tests/generics/syntax/variance_covariant.phpt create mode 100644 Zend/tests/generics/syntax/variance_invariant_default.phpt create mode 100644 Zend/tests/generics/traits/abstract_method_implemented.phpt create mode 100644 Zend/tests/generics/traits/abstract_method_two_classes.phpt create mode 100644 Zend/tests/generics/traits/abstract_method_violates.phpt create mode 100644 Zend/tests/generics/traits/basic_trait.phpt create mode 100644 Zend/tests/generics/traits/method_concrete_unchanged.phpt create mode 100644 Zend/tests/generics/traits/method_default_arg.phpt create mode 100644 Zend/tests/generics/traits/method_signature_substituted.phpt create mode 100644 Zend/tests/generics/traits/method_variadic_substituted.phpt create mode 100644 Zend/tests/generics/traits/multiple_traits.phpt create mode 100644 Zend/tests/generics/traits/property_no_substitution_when_concrete.phpt create mode 100644 Zend/tests/generics/traits/property_substituted_two_classes.phpt create mode 100644 Zend/tests/generics/traits/property_substituted_with_class_arg.phpt create mode 100644 Zend/tests/generics/traits/trait_constant_type.phpt create mode 100644 Zend/tests/generics/traits/trait_get_generic_params.phpt create mode 100644 Zend/tests/generics/traits/trait_isgeneric.phpt create mode 100644 Zend/tests/generics/traits/trait_method_isgeneric.phpt create mode 100644 Zend/tests/generics/traits/trait_method_runtime.phpt create mode 100644 Zend/tests/generics/traits/trait_property_substituted.phpt create mode 100644 Zend/tests/generics/traits/trait_property_type.phpt create mode 100644 Zend/tests/generics/traits/trait_use_args_compile.phpt create mode 100644 Zend/tests/generics/traits/trait_with_method_param.phpt create mode 100644 Zend/tests/generics/turbofish/arity/attribute_arity.phpt create mode 100644 Zend/tests/generics/turbofish/arity/below_required.phpt create mode 100644 Zend/tests/generics/turbofish/arity/function_too_few.phpt create mode 100644 Zend/tests/generics/turbofish/arity/function_too_many.phpt create mode 100644 Zend/tests/generics/turbofish/arity/method_arity.phpt create mode 100644 Zend/tests/generics/turbofish/arity/new_arity.phpt create mode 100644 Zend/tests/generics/turbofish/arity/non_generic_callee.phpt create mode 100644 Zend/tests/generics/turbofish/arity/with_defaults.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/attribute_satisfies.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/attribute_under_opcache.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/attribute_violates.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/error_renders_args.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/function_call_violates.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/method_call_violates.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/multi_param_first_violates.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/multi_param_second_violates.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/new_satisfies.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/new_violates.phpt create mode 100644 Zend/tests/generics/turbofish/bounds/no_bound_no_check.phpt create mode 100644 Zend/tests/generics/turbofish/composite_args.phpt create mode 100644 Zend/tests/generics/turbofish/fcc_function.phpt create mode 100644 Zend/tests/generics/turbofish/fcc_method.phpt create mode 100644 Zend/tests/generics/turbofish/fcc_static_method.phpt create mode 100644 Zend/tests/generics/turbofish/function_call.phpt create mode 100644 Zend/tests/generics/turbofish/function_call_multi.phpt create mode 100644 Zend/tests/generics/turbofish/in_chain.phpt create mode 100644 Zend/tests/generics/turbofish/instance_method_call.phpt create mode 100644 Zend/tests/generics/turbofish/instanceof_with_args.phpt create mode 100644 Zend/tests/generics/turbofish/many_type_args.phpt create mode 100644 Zend/tests/generics/turbofish/nested_args.phpt create mode 100644 Zend/tests/generics/turbofish/new_with_args.phpt create mode 100644 Zend/tests/generics/turbofish/no_runtime_effect.phpt create mode 100644 Zend/tests/generics/turbofish/nullsafe_method_call.phpt create mode 100644 Zend/tests/generics/turbofish/static_method_call.phpt create mode 100644 Zend/tests/generics/turbofish/turbofish_in_attribute.phpt create mode 100644 Zend/tests/generics/turbofish/with_named_args.phpt create mode 100644 Zend/tests/generics/turbofish/with_spread.phpt create mode 100644 docs/source/core/generics.rst create mode 100644 ext/reflection/tests/generics/ancestor_args_composite.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_duplicate_bindings.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_nested.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_nested_transitive.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_no_generics_class.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_parent_class.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_parent_interface.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_transitive_paths.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_type_param_refs.phpt create mode 100644 ext/reflection/tests/generics/ancestor_args_used_trait.phpt create mode 100644 ext/reflection/tests/generics/arrow_fn_get_generic_params.phpt create mode 100644 ext/reflection/tests/generics/class_isgeneric_false.phpt create mode 100644 ext/reflection/tests/generics/class_isgeneric_true.phpt create mode 100644 ext/reflection/tests/generics/closure_get_generic_params.phpt create mode 100644 ext/reflection/tests/generics/composite_bound.phpt create mode 100644 ext/reflection/tests/generics/composite_type_arg.phpt create mode 100644 ext/reflection/tests/generics/function_isgeneric.phpt create mode 100644 ext/reflection/tests/generics/get_generic_parameters_count.phpt create mode 100644 ext/reflection/tests/generics/get_generic_parameters_empty.phpt create mode 100644 ext/reflection/tests/generics/method_get_generic_params.phpt create mode 100644 ext/reflection/tests/generics/method_isgeneric.phpt create mode 100644 ext/reflection/tests/generics/named_type_get_name_erased.phpt create mode 100644 ext/reflection/tests/generics/named_type_has_args.phpt create mode 100644 ext/reflection/tests/generics/named_type_no_args.phpt create mode 100644 ext/reflection/tests/generics/nested_args.phpt create mode 100644 ext/reflection/tests/generics/parameter_constructor_throws.phpt create mode 100644 ext/reflection/tests/generics/parameter_get_bound.phpt create mode 100644 ext/reflection/tests/generics/parameter_get_declaring_class.phpt create mode 100644 ext/reflection/tests/generics/parameter_get_declaring_function.phpt create mode 100644 ext/reflection/tests/generics/parameter_get_position.phpt create mode 100644 ext/reflection/tests/generics/parameter_get_variance.phpt create mode 100644 ext/reflection/tests/generics/parameter_has_bound.phpt create mode 100644 ext/reflection/tests/generics/parameter_has_default.phpt create mode 100644 ext/reflection/tests/generics/parameter_to_string.phpt create mode 100644 ext/reflection/tests/generics/property_type_args.phpt create mode 100644 ext/reflection/tests/generics/type_parameter_ref_get_param.phpt create mode 100644 ext/reflection/tests/generics/type_parameter_reference.phpt create mode 100644 ext/reflection/tests/generics/variance_enum.phpt diff --git a/NEWS b/NEWS index 2bbe43032ece..dc887cba05cf 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,14 @@ PHP NEWS ?? ??? ????, PHP 8.6.0alpha1 - Core: + . Added generics: type parameters on classes, interfaces, traits, functions, + methods, closures, and arrow functions, with optional bounds, defaults, + and variance markers; turbofish syntax (`f::()`) at call sites; and + type arguments on named types (`Box`, `array`, `iterable`, + `self`, `static`, `parent`). Type parameters erase to their bound + at runtime; type arguments are discarded. Pre-erasure metadata is preserved + for Reflection so static-analysis tools can consume generics without + re-parsing source. (azjezz) . Added first-class callable cache to share instances for the duration of the request. (ilutov) . It is now possible to use reference assign on WeakMap without the key diff --git a/UPGRADING b/UPGRADING index bdadc6efbefc..b0b7f1cca960 100644 --- a/UPGRADING +++ b/UPGRADING @@ -177,6 +177,31 @@ PHP 8.6 UPGRADE NOTES ======================================== - Core: + . Added support for runtime-bound-checked generics. Classes, interfaces, + traits, functions, methods, closures, and arrow functions can now declare + type parameters with optional bounds (`T : Foo`), defaults (`T = int`), + and variance markers (`+T`, `-T`): + + class Box { + public T $value; + public function get(): T { return $this->value; } + } + + function id(T $x): T { return $x; } + + Call sites accept turbofish type arguments (`Box::::new()`, + `id::(7)`); use sites accept type arguments on named types + (`Box`, `array`, `iterable`, `self`, + `static`, `parent`). Recursive bounds (`T : Comparable`) + are supported. Anonymous classes cannot declare type parameters. + + At runtime each type parameter is replaced by its declared bound + (or `mixed` when unbounded, or when the bound is invalid in the + target position, e.g. `callable` on a property), and type + arguments are discarded. Pre-erasure metadata is preserved on + functions, methods, and class entries and is exposed through + Reflection so that PHP-based static-analysis tools can consume + generics without re-parsing source. . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. . It is now possible to define the `__debugInfo()` magic method on enums. @@ -315,6 +340,23 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/isreadable-iswriteable . Added ReflectionParameter::getDocComment(). RFC: https://wiki.php.net/rfc/parameter-doccomments + . Added ReflectionFunctionAbstract::isGeneric() and + ReflectionFunctionAbstract::getGenericParameters() (covers + ReflectionFunction, ReflectionMethod, closures, and arrow functions). + . Added ReflectionClass::isGeneric() and + ReflectionClass::getGenericParameters(). + . Added ReflectionClass::getGenericArgumentsForParentClass(), + ReflectionClass::getGenericArgumentsForParentInterface(string $name), + and ReflectionClass::getGenericArgumentsForUsedTrait(string $name) for + inspecting the type arguments supplied at a class's own extends / + implements / use sites. Returns null when no type arguments were + specified for that ancestor at this class's clause site (consumers + enumerate ancestors via the existing getParentClass() / getInterfaces() + / getTraits() APIs). + . Added ReflectionNamedType::hasGenericArguments() and + ReflectionNamedType::getGenericArguments(). The arguments are returned + as ReflectionType instances in source order (pre-erasure form); + ReflectionNamedType::getName() continues to return the erased name. - Intl: . `grapheme_strrev()` returns strrev for grapheme cluster unit. @@ -346,6 +388,15 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/tls_session_resumption . Openssl\Psk +- Reflection: + . ReflectionGenericTypeParameter (final, instances obtained via + ReflectionClass::getGenericParameters() and + ReflectionFunctionAbstract::getGenericParameters()). + . ReflectionTypeParameterReference (extends ReflectionType, appears only + inside pre-erasure type expressions: bounds, defaults, and the elements + of ReflectionNamedType::getGenericArguments()). + . enum ReflectionGenericVariance { Invariant; Covariant; Contravariant }. + - Standard: . enum SortDirection RFC: https://wiki.php.net/rfc/sort_direction_enum diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 1f24fb82fcf9..d06308b18bf0 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -117,6 +117,54 @@ PHP 8.6 INTERNALS UPGRADE NOTES . Added ZEND_CONTAINER_OF(). . The OPENBASEDIR_CHECKPATH() compatibility macro has been removed, instead use php_check_open_basedir() directly. + . Added support for runtime-bound-checked generic type parameters. The + main additions: + . New types in zend_compile.h: zend_generic_parameter, + zend_generic_parameter_list, zend_generic_type_table, and + zend_generic_scope_entry. Allocate / destroy via + zend_generic_parameter_list_alloc(), + zend_generic_parameter_list_destroy(), + zend_generic_type_table_alloc(), and + zend_generic_type_table_destroy(). + . zend_op_array and zend_class_entry both gained an optional + `generic_parameters` (declared parameter list) and an optional + `generic_types` side table holding the pre-erasure forms of + return types, parameter types, property types, class-constant + types, the extends type, implements list, and trait-use list. + The runtime arg_info / property / class-constant slots continue + to hold only the erased form. + . New AST kinds: ZEND_AST_GENERIC_TYPE_PARAMETER_LIST, + ZEND_AST_GENERIC_TYPE_PARAMETER, ZEND_AST_GENERIC_NAMED_TYPE, + ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST, ZEND_AST_TURBOFISH. + . zend_ast_decl::child[] grew from 5 to 6 entries; the new slot + carries an optional generic-parameter-list AST. + . The child-count groups of ZEND_AST_CALL, ZEND_AST_NEW, + ZEND_AST_METHOD_CALL, ZEND_AST_NULLSAFE_METHOD_CALL, and + ZEND_AST_STATIC_CALL each gained one optional child holding the + call-site turbofish type-argument list. Code that walks these + nodes by hard-coded child count must be updated. + . zend_ast_export handles the new generic AST kinds. + . Two new bits on zend_type's type_mask: + _ZEND_TYPE_TYPE_PARAMETER_BIT (1u << 25) and + _ZEND_TYPE_NAMED_WITH_ARGS_BIT (1u << 31), with payload structs + zend_type_parameter_ref { zend_string *name; uint32_t index; + uint8_t origin; } and zend_type_named_with_args { zend_string + *name; uint32_t name_attr; uint32_t count; zend_type args[]; }. + These bits only ever appear in pre-erasure forms held by the + side table; runtime arg_info / property / class-constant types + never carry them. Helpers: ZEND_TYPE_HAS_TYPE_PARAMETER(), + ZEND_TYPE_TYPE_PARAMETER(), ZEND_TYPE_HAS_NAMED_WITH_ARGS(), + ZEND_TYPE_NAMED_WITH_ARGS(). + . New compiler-globals fields: CG(type_arg_depth) (right-angle + split state used by the zendlex wrapper), CG(token_residual) + (single-token pushback slot), and CG(generic_scope) (linked + stack of in-scope type parameters). + . New T_TURBOFISH lexer token (literal `::<`). The zendlex wrapper + splits T_SR (`>>`), T_IS_GREATER_OR_EQUAL (`>=`), and T_SR_EQUAL + (`>>=`) into separate `>` tokens whenever CG(type_arg_depth) is + non-zero, with a single-token pushback slot. + . Module API bumped to 20260506; extension API bumped to + 420260506. All extensions must be recompiled. ======================== 2. Build system changes diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c index 69c371207ddc..bcbb8e07b913 100644 --- a/Zend/Optimizer/optimize_func_calls.c +++ b/Zend/Optimizer/optimize_func_calls.c @@ -74,6 +74,46 @@ static void zend_delete_call_instructions(const zend_op_array *op_array, zend_op } } +/* Returns true if a VERIFY_GENERIC_ARGUMENTS sits between this call's INIT and + * DO opcodes; such a call cannot be inlined because the verify opcode reads + * EX(call), which goes away once the frame is dropped. */ +static bool zend_call_has_generic_arguments_check(zend_op *opline) +{ + int call = 0; + while (1) { + switch (opline->opcode) { + case ZEND_INIT_FCALL_BY_NAME: + case ZEND_INIT_NS_FCALL_BY_NAME: + case ZEND_INIT_STATIC_METHOD_CALL: + case ZEND_INIT_METHOD_CALL: + case ZEND_INIT_FCALL: + case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: + if (call == 0) { + return false; + } + ZEND_FALLTHROUGH; + case ZEND_NEW: + case ZEND_INIT_DYNAMIC_CALL: + case ZEND_INIT_USER_CALL: + call--; + break; + case ZEND_DO_FCALL: + case ZEND_DO_ICALL: + case ZEND_DO_UCALL: + case ZEND_DO_FCALL_BY_NAME: + call++; + break; + case ZEND_VERIFY_GENERIC_ARGUMENTS: + if (call == 0) { + return true; + } + break; + } + + opline--; + } +} + static void zend_try_inline_call(zend_op_array *op_array, const zend_op *fcall, zend_op *opline, const zend_function *func) { const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; @@ -97,6 +137,13 @@ static void zend_try_inline_call(zend_op_array *op_array, const zend_op *fcall, return; } + if (op_array->generic_types + && op_array->generic_types->turbofish_args + && zend_call_has_generic_arguments_check(opline - 1)) { + /* The verify opcode must run; inlining would orphan it. */ + return; + } + for (i = 0; i < num_args; i++) { /* Don't inline functions with by-reference arguments. This would require * correct handling of INDIRECT arguments. */ diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index d10b4d83fc3e..df472bed3040 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -1734,12 +1734,45 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l uint32_t fn_flags2 = op_array->fn_flags2; zend_function *prototype = op_array->prototype; HashTable *ht = op_array->static_variables; + zend_arg_info *arg_info = op_array->arg_info; + bool arg_info_substituted = (arg_info != orig_op_array->arg_info); *op_array = *orig_op_array; op_array->fn_flags = fn_flags; op_array->fn_flags2 = fn_flags2; op_array->prototype = prototype; op_array->static_variables = ht; + if (arg_info_substituted) { + op_array->arg_info = arg_info; + } + } + } + } ZEND_HASH_FOREACH_END(); + + zend_property_info *prop; + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, name, prop) { + if (!(prop->flags & ZEND_ACC_GENERIC_CLONE) || !prop->hooks) { + continue; + } + + const zend_property_info *parent_prop = zend_hash_find_ptr(&prop->ce->properties_info, name); + if (!parent_prop || !parent_prop->hooks) { + continue; + } + + for (uint32_t hi = 0; hi < ZEND_PROPERTY_HOOK_COUNT; hi++) { + zend_function *clone_hook = prop->hooks[hi]; + zend_function *parent_hook = parent_prop->hooks[hi]; + if (!clone_hook || !parent_hook || clone_hook == parent_hook) { + continue; + } + + zend_arg_info *arg_info = clone_hook->op_array.arg_info; + bool arg_info_substituted = (arg_info != parent_hook->op_array.arg_info); + + clone_hook->op_array = parent_hook->op_array; + if (arg_info_substituted) { + clone_hook->op_array.arg_info = arg_info; } } } ZEND_HASH_FOREACH_END(); diff --git a/Zend/tests/generics/declaration/default_satisfies_bound.phpt b/Zend/tests/generics/declaration/default_satisfies_bound.phpt new file mode 100644 index 000000000000..6aebf72326c5 --- /dev/null +++ b/Zend/tests/generics/declaration/default_satisfies_bound.phpt @@ -0,0 +1,18 @@ +--TEST-- +Declaration: type-parameter default that satisfies its bound is accepted +--FILE-- + {} +function f(): void {} +trait Tr {} +interface I {} + +new Box; +f(); +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/declaration/default_violates_bound_class.phpt b/Zend/tests/generics/declaration/default_violates_bound_class.phpt new file mode 100644 index 000000000000..17c79f43bd7d --- /dev/null +++ b/Zend/tests/generics/declaration/default_violates_bound_class.phpt @@ -0,0 +1,9 @@ +--TEST-- +Declaration: class type-parameter default that does not satisfy its bound is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d diff --git a/Zend/tests/generics/declaration/default_violates_bound_function.phpt b/Zend/tests/generics/declaration/default_violates_bound_function.phpt new file mode 100644 index 000000000000..ad83c63395ec --- /dev/null +++ b/Zend/tests/generics/declaration/default_violates_bound_function.phpt @@ -0,0 +1,9 @@ +--TEST-- +Declaration: function type-parameter default that does not satisfy its bound is rejected +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d diff --git a/Zend/tests/generics/declaration/default_violates_bound_interface.phpt b/Zend/tests/generics/declaration/default_violates_bound_interface.phpt new file mode 100644 index 000000000000..0339c260e029 --- /dev/null +++ b/Zend/tests/generics/declaration/default_violates_bound_interface.phpt @@ -0,0 +1,9 @@ +--TEST-- +Declaration: interface type-parameter default that does not satisfy its bound is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d diff --git a/Zend/tests/generics/declaration/default_violates_bound_trait.phpt b/Zend/tests/generics/declaration/default_violates_bound_trait.phpt new file mode 100644 index 000000000000..656fa275618d --- /dev/null +++ b/Zend/tests/generics/declaration/default_violates_bound_trait.phpt @@ -0,0 +1,9 @@ +--TEST-- +Declaration: trait type-parameter default that does not satisfy its bound is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d diff --git a/Zend/tests/generics/declaration/default_with_intersection_bound.phpt b/Zend/tests/generics/declaration/default_with_intersection_bound.phpt new file mode 100644 index 000000000000..a493c0003646 --- /dev/null +++ b/Zend/tests/generics/declaration/default_with_intersection_bound.phpt @@ -0,0 +1,8 @@ +--TEST-- +Declaration: default checked against intersection bound when types are concrete +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Traversable&Countable in %s on line %d diff --git a/Zend/tests/generics/declaration/default_with_union_bound.phpt b/Zend/tests/generics/declaration/default_with_union_bound.phpt new file mode 100644 index 000000000000..5477170bca2d --- /dev/null +++ b/Zend/tests/generics/declaration/default_with_union_bound.phpt @@ -0,0 +1,8 @@ +--TEST-- +Declaration: default checked against union bound when types are concrete +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default float for type parameter T does not satisfy its bound string|int in %s on line %d diff --git a/Zend/tests/generics/declaration/error_renders_default_args.phpt b/Zend/tests/generics/declaration/error_renders_default_args.phpt new file mode 100644 index 000000000000..e92783bbcfc3 --- /dev/null +++ b/Zend/tests/generics/declaration/error_renders_default_args.phpt @@ -0,0 +1,9 @@ +--TEST-- +Bound error message: declaration-time default-vs-bound error renders NAMED_WITH_ARGS in the bound +--FILE-- + {} +class Box = int> {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Comparable in %s on line %d diff --git a/Zend/tests/generics/declaration/no_leak_dnf_bound.phpt b/Zend/tests/generics/declaration/no_leak_dnf_bound.phpt new file mode 100644 index 000000000000..14c7181fa8ce --- /dev/null +++ b/Zend/tests/generics/declaration/no_leak_dnf_bound.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generics: declaring a class with a DNF-typed bound does not leak memory +--FILE-- + { + public function take(T $x): void {} + public function get(): T {} +} + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/no_leak_intersection_bound.phpt b/Zend/tests/generics/declaration/no_leak_intersection_bound.phpt new file mode 100644 index 000000000000..19c8197a8612 --- /dev/null +++ b/Zend/tests/generics/declaration/no_leak_intersection_bound.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generics: declaring a class with an intersection-typed bound does not leak memory +--FILE-- + { + public function take(T $x): void {} +} + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/no_leak_union_bound.phpt b/Zend/tests/generics/declaration/no_leak_union_bound.phpt new file mode 100644 index 000000000000..de18838ff1eb --- /dev/null +++ b/Zend/tests/generics/declaration/no_leak_union_bound.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generics: declaring a class with a union-typed bound does not leak memory +--FILE-- + { + public function take(T $x): void {} +} + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/chain_forward.phpt b/Zend/tests/generics/declaration/recursive_bounds/chain_forward.phpt new file mode 100644 index 000000000000..9a5625588b06 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/chain_forward.phpt @@ -0,0 +1,11 @@ +--TEST-- +Recursive bounds: chain of forward references through three parameters +--FILE-- + {} +class Foo {} +function f, U: Box, V: Foo>(T $a, U $b, V $c): void {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/default_backward_ref_allowed.phpt b/Zend/tests/generics/declaration/recursive_bounds/default_backward_ref_allowed.phpt new file mode 100644 index 000000000000..239f16563044 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/default_backward_ref_allowed.phpt @@ -0,0 +1,11 @@ +--TEST-- +Recursive bounds: backward reference in a default is allowed +--FILE-- + {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/default_forward_ref_rejected.phpt b/Zend/tests/generics/declaration/recursive_bounds/default_forward_ref_rejected.phpt new file mode 100644 index 000000000000..206ae5157b64 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/default_forward_ref_rejected.phpt @@ -0,0 +1,9 @@ +--TEST-- +Recursive bounds: forward reference in a default is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type parameter U referenced before declaration in %s on line %d diff --git a/Zend/tests/generics/declaration/recursive_bounds/direct_self_ref_still_rejected.phpt b/Zend/tests/generics/declaration/recursive_bounds/direct_self_ref_still_rejected.phpt new file mode 100644 index 000000000000..f6d669013f1a --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/direct_self_ref_still_rejected.phpt @@ -0,0 +1,8 @@ +--TEST-- +Recursive bounds: direct self-reference in a bound (not inside a type argument) is still rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt b/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt new file mode 100644 index 000000000000..67b2f02ac727 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt @@ -0,0 +1,17 @@ +--TEST-- +Recursive bounds: a child class can extend a parent that uses mutual bounds +--FILE-- + {} +class Pair, U: Box> { + public function __construct(public Box $left, public Box $right) {} +} + +class StrictPair, B: Box> extends Pair {} + +$p = new StrictPair::, Box>(new Box(), new Box()); +var_dump($p instanceof Pair, $p instanceof StrictPair); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/declaration/recursive_bounds/forward_bound_with_class_bound.phpt b/Zend/tests/generics/declaration/recursive_bounds/forward_bound_with_class_bound.phpt new file mode 100644 index 000000000000..51d7330fb619 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/forward_bound_with_class_bound.phpt @@ -0,0 +1,18 @@ +--TEST-- +Recursive bounds: forward reference where the target has a non-generic class bound +--FILE-- +(T $x): U { return $x; } +var_dump(f(new Foo())); +try { + f(new Bar()); +} catch (TypeError $e) { + echo "type error\n"; +} +?> +--EXPECTF-- +object(Foo)#%d (0) { +} +type error diff --git a/Zend/tests/generics/declaration/recursive_bounds/forward_in_generic_arg.phpt b/Zend/tests/generics/declaration/recursive_bounds/forward_in_generic_arg.phpt new file mode 100644 index 000000000000..86f7feb081f0 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/forward_in_generic_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: forward reference nested inside generic type argument +--FILE-- + {} +function g, T>(U $x): void {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/forward_simple.phpt b/Zend/tests/generics/declaration/recursive_bounds/forward_simple.phpt new file mode 100644 index 000000000000..7438e337627b --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/forward_simple.phpt @@ -0,0 +1,11 @@ +--TEST-- +Recursive bounds: simple forward reference in bound +--FILE-- +(U $x): T { return $x; } +var_dump(f(new Foo())); +?> +--EXPECTF-- +object(Foo)#%d (0) { +} diff --git a/Zend/tests/generics/declaration/recursive_bounds/four_way_cycle.phpt b/Zend/tests/generics/declaration/recursive_bounds/four_way_cycle.phpt new file mode 100644 index 000000000000..af67dbf89f66 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/four_way_cycle.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: four-parameter cycle A -> B -> C -> D -> A +--FILE-- + {} +class Quad, B: Box, C: Box, D: Box> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/hammer_combined.phpt b/Zend/tests/generics/declaration/recursive_bounds/hammer_combined.phpt new file mode 100644 index 000000000000..4dcbd2c34460 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/hammer_combined.phpt @@ -0,0 +1,34 @@ +--TEST-- +Recursive bounds: combined exercise — class, interface, generic methods, multi-cycle +--FILE-- + {} +class IntBox implements Box {} +class StrBox implements Box {} + +interface Pair, U: Box> { + public function left(): Box; + public function right(): Box; +} + +class IntStrPair implements Pair { + public function __construct(private Box $l, private Box $r) {} + public function left(): Box { return $this->l; } + public function right(): Box { return $this->r; } +} + +class Container { + public function swap, B: Box>(Pair $p): array { + return [$p->right(), $p->left()]; + } +} + +$p = new IntStrPair(new IntBox(), new StrBox()); +$c = new Container::(); +[$first, $second] = $c->swap($p); + +var_dump($first instanceof StrBox, $second instanceof IntBox); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/declaration/recursive_bounds/interface_with_mutual_bounds.phpt b/Zend/tests/generics/declaration/recursive_bounds/interface_with_mutual_bounds.phpt new file mode 100644 index 000000000000..9d767f9f7c73 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/interface_with_mutual_bounds.phpt @@ -0,0 +1,25 @@ +--TEST-- +Recursive bounds: an interface can declare mutual bounds +--FILE-- + {} +interface PairLike, U: Box> { + public function left(): Box; + public function right(): Box; +} + +class IntBox implements Box {} +class StrBox implements Box {} + +class Pair implements PairLike { + public function __construct(private Box $l, private Box $r) {} + public function left(): Box { return $this->l; } + public function right(): Box { return $this->r; } +} + +$p = new Pair(new IntBox(), new StrBox()); +var_dump($p->left() instanceof IntBox, $p->right() instanceof StrBox); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_class.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_class.phpt new file mode 100644 index 000000000000..631c51afc2e5 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_class.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: class with mutually recursive type parameters +--FILE-- + {} +class Pair, U: Box> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_function.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_function.phpt new file mode 100644 index 000000000000..28377df49805 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_function.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: function with mutually recursive type parameters +--FILE-- + {} +function f, U: Box>(T $x, U $y): void {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_intersection_bound.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_intersection_bound.phpt new file mode 100644 index 000000000000..6d6c3dea0a3c --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_intersection_bound.phpt @@ -0,0 +1,11 @@ +--TEST-- +Recursive bounds: mutual references inside intersection bounds +--FILE-- + {} +interface Tagged {} +class Pair&Tagged, U: Box&Tagged> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_method.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_method.phpt new file mode 100644 index 000000000000..cbe70255fc6e --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_method.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: method with mutually recursive type parameters +--FILE-- + {} +class C { + public function f, U: Box>(T $x, U $y): void {} +} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_union_bound.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_union_bound.phpt new file mode 100644 index 000000000000..ae95eb6e79c9 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_union_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: mutual references inside union bounds +--FILE-- + {} +class Pair|null, U: Box|null> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/nested_mutual_in_outer.phpt b/Zend/tests/generics/declaration/recursive_bounds/nested_mutual_in_outer.phpt new file mode 100644 index 000000000000..d084c1274ad9 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/nested_mutual_in_outer.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: outer-scope class param appears inside mutually recursive method bounds +--FILE-- + {} +class Carrier> { + public function pair, U: Box>(T $a, U $b, O $c): void {} +} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/outer_scope_param_in_recursive_bound.phpt b/Zend/tests/generics/declaration/recursive_bounds/outer_scope_param_in_recursive_bound.phpt new file mode 100644 index 000000000000..6638805dc595 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/outer_scope_param_in_recursive_bound.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: an enclosing class's type parameter can appear inside a recursive method bound +--FILE-- + {} +class C { + public function f, U: Box>(T $a, U $b, O $c): void {} +} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/redeclare_still_rejected.phpt b/Zend/tests/generics/declaration/recursive_bounds/redeclare_still_rejected.phpt new file mode 100644 index 000000000000..1d3b690a63ff --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/redeclare_still_rejected.phpt @@ -0,0 +1,8 @@ +--TEST-- +Recursive bounds: duplicate parameter names are still rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Cannot redeclare type parameter T in %s on line %d diff --git a/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt b/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt new file mode 100644 index 000000000000..db3d865da3b9 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt @@ -0,0 +1,20 @@ +--TEST-- +Recursive bounds: a class with mutually recursive bounds can be instantiated and used +--FILE-- + { + public function __construct(public mixed $value) {} +} + +final class Pair, U: Box> { + public function __construct(public Box $left, public Box $right) {} +} + +$bx = new Box(1); +$by = new Box("hi"); +$p = new Pair($bx, $by); +var_dump($p->left->value, $p->right->value); +?> +--EXPECT-- +int(1) +string(2) "hi" diff --git a/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt b/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt new file mode 100644 index 000000000000..b50f0f59404e --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: self-reference inside a generic type argument is allowed (pre-existing behavior) +--FILE-- + {} +class Foo> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/triple_cycle.phpt b/Zend/tests/generics/declaration/recursive_bounds/triple_cycle.phpt new file mode 100644 index 000000000000..fa89a02ab6b6 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/triple_cycle.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: three-way cycle T -> U -> V -> T +--FILE-- + {} +class Cycle, U: Box, V: Box> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/turbofish_satisfied.phpt b/Zend/tests/generics/declaration/recursive_bounds/turbofish_satisfied.phpt new file mode 100644 index 000000000000..22145614882e --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/turbofish_satisfied.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: turbofish argument satisfying the head class is accepted +--FILE-- + {} +class Pair, U: Box> {} + +$p = new Pair::, Box>(); +var_dump($p instanceof Pair); +?> +--EXPECT-- +bool(true) diff --git a/Zend/tests/generics/declaration/recursive_bounds/turbofish_violation.phpt b/Zend/tests/generics/declaration/recursive_bounds/turbofish_violation.phpt new file mode 100644 index 000000000000..dd8c130d9792 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/turbofish_violation.phpt @@ -0,0 +1,15 @@ +--TEST-- +Recursive bounds: turbofish argument still must satisfy each parameter's bound +--FILE-- + {} +class Pair, U: Box> {} + +try { + $p = new Pair::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +Type argument 1 to new Pair does not satisfy the bound Box on parameter T, int given diff --git a/Zend/tests/generics/declaration/recursive_bounds/with_variance_covariant_violation.phpt b/Zend/tests/generics/declaration/recursive_bounds/with_variance_covariant_violation.phpt new file mode 100644 index 000000000000..fc3189b3f784 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/with_variance_covariant_violation.phpt @@ -0,0 +1,9 @@ +--TEST-- +Recursive bounds: covariant marker in bound position is still rejected (bounds are invariant) +--FILE-- + {} +class Pair<+T: Box, +U: Box> {} +?> +--EXPECTF-- +Fatal error: Type parameter %s declared covariant (+%s) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/recursive_bounds/with_variance_invariant.phpt b/Zend/tests/generics/declaration/recursive_bounds/with_variance_invariant.phpt new file mode 100644 index 000000000000..9235476a5d82 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/with_variance_invariant.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: invariant class params with mutual bounds are accepted +--FILE-- + {} +class Pair, U: Box> { + public function __construct(public Box $left, public Box $right) {} +} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/arrow_violation_covariant_in_param.phpt b/Zend/tests/generics/declaration/variance/arrow_violation_covariant_in_param.phpt new file mode 100644 index 000000000000..c8ab23de97aa --- /dev/null +++ b/Zend/tests/generics/declaration/variance/arrow_violation_covariant_in_param.phpt @@ -0,0 +1,8 @@ +--TEST-- +Variance: +T on an arrow function in parameter position is rejected +--FILE-- +(T $x): T => $x; +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_forwarded.phpt b/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_forwarded.phpt new file mode 100644 index 000000000000..4b3c255ce18a --- /dev/null +++ b/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_forwarded.phpt @@ -0,0 +1,20 @@ +--TEST-- +Variance: covariant T forwarded into an `implements` arg whose target autoloads later is accepted +--FILE-- + 'interface AUI_Sequence<+T> {}', + 'AUI_ArraySequence' => 'final class AUI_ArraySequence<+T> implements AUI_Sequence {}', + ]; + + if (isset($sources[$class])) { + eval($sources[$class]); + } +}); + +class_exists('AUI_ArraySequence'); +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_violation.phpt b/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_violation.phpt new file mode 100644 index 000000000000..2f32f90a6cfc --- /dev/null +++ b/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_violation.phpt @@ -0,0 +1,19 @@ +--TEST-- +Variance: covariant T forwarded into a contravariant `implements` arg is rejected at link time when the target autoloads later +--FILE-- + 'interface AUI2_Sink<-U> {}', + 'AUI2_Bad' => 'final class AUI2_Bad<+T> implements AUI2_Sink {}', + ]; + + if (isset($sources[$class])) { + eval($sources[$class]); + } +}); + +class_exists('AUI2_Bad'); +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/closure_violation_covariant_in_param.phpt b/Zend/tests/generics/declaration/variance/closure_violation_covariant_in_param.phpt new file mode 100644 index 000000000000..2edff154047b --- /dev/null +++ b/Zend/tests/generics/declaration/variance/closure_violation_covariant_in_param.phpt @@ -0,0 +1,8 @@ +--TEST-- +Variance: +T on a closure in parameter position is rejected +--FILE-- +(T $x): void {}; +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/function_happy_path.phpt b/Zend/tests/generics/declaration/variance/function_happy_path.phpt new file mode 100644 index 000000000000..ccd63d67e2ba --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_happy_path.phpt @@ -0,0 +1,29 @@ +--TEST-- +Variance: function-origin type parameters checked uniformly with class-origin +--FILE-- +(T $x): int { return 0; } +function make<+T>(): T { return null; } +function transform<-I, +O>(I $input): O { return null; } + +$closure_take = function <-T>(T $x): int { return 0; }; +$closure_make = function <+T>(): T { return null; }; +$arrow_take = fn<-T>(T $x): int => 0; +$arrow_make = fn<+T>(): T => null; + +class A<+T> { + public function map<-A_, +B>(A_ $a): B { return null; } +} + +take(42); +make(); +transform(1); +$closure_take(7); +$closure_make(); +$arrow_take(7); +$arrow_make(); +(new A)->map(42); +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/function_violation_composed.phpt b/Zend/tests/generics/declaration/variance/function_violation_composed.phpt new file mode 100644 index 000000000000..7b789e03a84a --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_violation_composed.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: function-origin +T composed through Consumer<-U> in return is rejected +--FILE-- + { public function consume(U $x): void; } + +function f<+T>(): Consumer {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/function_violation_contravariant_in_return.phpt b/Zend/tests/generics/declaration/variance/function_violation_contravariant_in_return.phpt new file mode 100644 index 000000000000..2777162dfe41 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_violation_contravariant_in_return.phpt @@ -0,0 +1,8 @@ +--TEST-- +Variance: -T on a function in return position (covariant) is rejected +--FILE-- +(): T {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in covariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/function_violation_covariant_in_param.phpt b/Zend/tests/generics/declaration/variance/function_violation_covariant_in_param.phpt new file mode 100644 index 000000000000..8cbbac94eff8 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_violation_covariant_in_param.phpt @@ -0,0 +1,8 @@ +--TEST-- +Variance: +T on a function in parameter position (contravariant) is rejected +--FILE-- +(T $x): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/function_violation_own_bound.phpt b/Zend/tests/generics/declaration/variance/function_violation_own_bound.phpt new file mode 100644 index 000000000000..38bd9ad201c4 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_violation_own_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T on a function appearing in its own bound (invariant position) is rejected +--FILE-- + {} + +function f<+T : Box>(): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/happy_path.phpt b/Zend/tests/generics/declaration/variance/happy_path.phpt new file mode 100644 index 000000000000..6dc0ed560b11 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/happy_path.phpt @@ -0,0 +1,87 @@ +--TEST-- +Variance: full happy-path showing every legal combination +--FILE-- + { public function produce(): T; } +interface Consumer<-T> { public function consume(T $x): void; } +interface Inv { public function rw(U $x): U; } + +class CoProducer<+T> { + public readonly T $tag; + public function __construct(T $tag) { $this->tag = $tag; } + public function get(): T {} + public function makeProducer(): Producer {} + public function makeConsumerOfConsumers(): Consumer> {} +} + +class ReadOnlyHooked<+T> { + private T $backing; + public function __construct(T $v) { $this->backing = $v; } + public T $val { get => $this->backing; } +} + +class CoConsumer<-T> { + public function take(T $x): void {} + public function takeMany(T ...$xs): void {} + public function makeConsumer(): Consumer {} + public function consumerOfProducers(Producer $p): void {} +} + +class Holder { + public T $val; + public function __construct(T $v) { $this->val = $v; } + public function get(): T { return $this->val; } + public function set(T $v): void { $this->val = $v; } + public T $hooked { + get => $this->val; + set(T $v) { $this->val = $v; } + } +} + +interface Source<+T> { public function fetch(): T; } +class StringSource implements Source { + public function fetch(): string { return "ok"; } +} + +class CovariantChain<+T> implements Source { + public function __construct(public readonly T $value) {} + public function fetch(): T { return $this->value; } +} + +class MethodLevel<+T> { + public function map(U $key): T {} +} + +trait TraitProducer<+U> { + public function produceU(): U {} +} +class UsesProducerTrait { + use TraitProducer; + public function produceU(): int { return 7; } +} + +$h = new Holder::(42); +$h->set(7); +echo $h->get(), "\n"; + +$rh = new ReadOnlyHooked::("hi"); +echo $rh->val, "\n"; + +$cp = new CoProducer::(new Dog()); +echo get_class($cp->tag), "\n"; + +echo (new UsesProducerTrait)->produceU(), "\n"; +echo (new StringSource)->fetch(), "\n"; + +echo "all variance combinations OK\n"; +?> +--EXPECT-- +7 +hi +Dog +7 +ok +all variance combinations OK diff --git a/Zend/tests/generics/declaration/variance/method_function_origin_violation.phpt b/Zend/tests/generics/declaration/variance/method_function_origin_violation.phpt new file mode 100644 index 000000000000..2d053516ebb8 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/method_function_origin_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T on a method's own type parameter in parameter position is rejected +--FILE-- +(T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt b/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt new file mode 100644 index 000000000000..fbc0d827d949 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt @@ -0,0 +1,13 @@ +--TEST-- +Variance: class-T and function-T variance markers are checked independently in the same method +--FILE-- + { + public function map<-A, +B>(A $a): B { return null; } +} + +(new C)->map(42); +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/parent_keyword_ok.phpt b/Zend/tests/generics/declaration/variance/parent_keyword_ok.phpt new file mode 100644 index 000000000000..de3a72a07366 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/parent_keyword_ok.phpt @@ -0,0 +1,16 @@ +--TEST-- +Variance: parent from a generic child of a generic parent +--FILE-- + { + public function __construct(public T $value) {} +} +final readonly class B<+T> extends A { + public function asParent(): parent { return $this; } +} + +$b = new B::(7); +echo $b->asParent()->value, "\n"; +?> +--EXPECT-- +7 diff --git a/Zend/tests/generics/declaration/variance/parent_keyword_violation.phpt b/Zend/tests/generics/declaration/variance/parent_keyword_violation.phpt new file mode 100644 index 000000000000..47c5217e614f --- /dev/null +++ b/Zend/tests/generics/declaration/variance/parent_keyword_violation.phpt @@ -0,0 +1,13 @@ +--TEST-- +Variance: parent in a contravariant parameter position is rejected for +T +--FILE-- + { + public function __construct(public T $value) {} +} +class B<+T> extends A { + public function take(parent $other): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/rfc_example.phpt b/Zend/tests/generics/declaration/variance/rfc_example.phpt new file mode 100644 index 000000000000..29aa6bfff6ba --- /dev/null +++ b/Zend/tests/generics/declaration/variance/rfc_example.phpt @@ -0,0 +1,45 @@ +--TEST-- +RFC example: covariant Pair/Box with self-referential return type, zip, map, identity +--FILE-- + { + public function __construct( + public L $left, + public R $right, + ) {} + + public function swap(): Pair { + return new Pair($this->right, $this->left); + } +} + +final readonly class Box<+T> { + public function __construct( + public T $value, + ) {} + + public function map(callable $fn): Box { + return new Box(($fn)($this->value)); + } + + public function zip(O $value): Box> { + return new Box(new Pair($this->value, $value)); + } +} + +function identity(T $value): T { + return $value; +} + +$greeting = new Box::("hello, world"); +$paired = $greeting->zip::(42); +$swapped = $paired->value->swap(); +$result = identity::>($swapped); + +var_dump($result->left); +var_dump($result->right); +?> +--EXPECT-- +int(42) +string(12) "hello, world" diff --git a/Zend/tests/generics/declaration/variance/self_keyword_ok.phpt b/Zend/tests/generics/declaration/variance/self_keyword_ok.phpt new file mode 100644 index 000000000000..62825c96bb2e --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_keyword_ok.phpt @@ -0,0 +1,14 @@ +--TEST-- +Variance: self in a covariant return position +--FILE-- + { + public function __construct(public T $value) {} + public function clone_(): self { return new self($this->value); } +} + +$c = new C::(42); +echo $c->clone_()->value, "\n"; +?> +--EXPECT-- +42 diff --git a/Zend/tests/generics/declaration/variance/self_keyword_through_invariant_class_violation.phpt b/Zend/tests/generics/declaration/variance/self_keyword_through_invariant_class_violation.phpt new file mode 100644 index 000000000000..7c579f8f4986 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_keyword_through_invariant_class_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T cannot appear at an invariant slot of self<...> when the class param is invariant +--FILE-- + { + public function get(): self {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/self_keyword_violation.phpt b/Zend/tests/generics/declaration/variance/self_keyword_violation.phpt new file mode 100644 index 000000000000..cc90c2ad7d0c --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_keyword_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: self in a contravariant parameter position is rejected for +T +--FILE-- + { + public function take(self $other): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt b/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt new file mode 100644 index 000000000000..9c88343c7ab8 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt @@ -0,0 +1,18 @@ +--TEST-- +Variance: a generic class referencing itself by name in a covariant return position +--FILE-- + { + public function __construct(public L $left, public R $right) {} + public function swap(): Pair { + return new Pair($this->right, $this->left); + } +} + +$p = new Pair::(7, "hi"); +$s = $p->swap(); +var_dump($s->left, $s->right); +?> +--EXPECT-- +string(2) "hi" +int(7) diff --git a/Zend/tests/generics/declaration/variance/self_ref_class_name_violation.phpt b/Zend/tests/generics/declaration/variance/self_ref_class_name_violation.phpt new file mode 100644 index 000000000000..505d5773102d --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_ref_class_name_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: self-referenced class name in a violating position is still rejected +--FILE-- + { + public function take(C $other): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/static_keyword_ok.phpt b/Zend/tests/generics/declaration/variance/static_keyword_ok.phpt new file mode 100644 index 000000000000..0fac59480454 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/static_keyword_ok.phpt @@ -0,0 +1,14 @@ +--TEST-- +Variance: static in a covariant return position +--FILE-- + { + public function __construct(public T $value) {} + public function dup(): static { return new static($this->value); } +} + +$c = new C::("ok"); +echo $c->dup()->value, "\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/static_method_function_origin_violation.phpt b/Zend/tests/generics/declaration/variance/static_method_function_origin_violation.phpt new file mode 100644 index 000000000000..f1998ace645a --- /dev/null +++ b/Zend/tests/generics/declaration/variance/static_method_function_origin_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T on a static method's own type parameter in parameter position is rejected +--FILE-- +(T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_in_get_hook.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_in_get_hook.phpt new file mode 100644 index 000000000000..48809790c6ff --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_in_get_hook.phpt @@ -0,0 +1,15 @@ +--TEST-- +Variance: -T in a hooked property with set hook (invariant position) is rejected +--FILE-- + { + private T $backing; + public function __construct(T $v) { $this->backing = $v; } + public T $val { + get => $this->backing; + set(T $v) { $this->backing = $v; } + } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_in_readonly_property.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_in_readonly_property.phpt new file mode 100644 index 000000000000..4f5e19309fa3 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_in_readonly_property.phpt @@ -0,0 +1,11 @@ +--TEST-- +Variance: -T in a readonly property (covariant position) is rejected +--FILE-- + { + public readonly T $val; + public function __construct(T $v) { $this->val = $v; } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in covariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_in_return.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_in_return.phpt new file mode 100644 index 000000000000..e70dd1668a48 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_in_return.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: -T in method return (covariant position) is rejected +--FILE-- + { + public function get(): T {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in covariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_in_rw_property.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_in_rw_property.phpt new file mode 100644 index 000000000000..4089e855215d --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_in_rw_property.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: -T in a read/write typed property (invariant position) is rejected +--FILE-- + { + public T $val; +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_through_covariant_arg_in_return.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_through_covariant_arg_in_return.phpt new file mode 100644 index 000000000000..d546fc82ec56 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_through_covariant_arg_in_return.phpt @@ -0,0 +1,12 @@ +--TEST-- +Variance: -T composed through Producer<+U> in return (+ × + = +) is rejected +--FILE-- + { public function produce(): U; } + +class A<-T> { + public function makeProducer(): Producer {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in covariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_byref_get_hook.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_byref_get_hook.phpt new file mode 100644 index 000000000000..d325cc358f94 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_byref_get_hook.phpt @@ -0,0 +1,14 @@ +--TEST-- +Variance: +T in a hooked property with by-ref get (effectively invariant) is rejected +--FILE-- + { + private T $backing; + public function __construct(T $v) { $this->backing = $v; } + public T $val { + &get => $this->backing; + } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_default.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_default.phpt new file mode 100644 index 000000000000..917bdf2d3fbb --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_default.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T in a sibling type parameter's default (invariant position) is rejected +--FILE-- + { public function get(): U; } + +class A<+T, U = Container> {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_own_bound.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_own_bound.phpt new file mode 100644 index 000000000000..137641603c97 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_own_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T appearing in its own bound (invariant position) is rejected +--FILE-- + { public function get(): U; } + +class A<+T : Box> {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_param.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_param.phpt new file mode 100644 index 000000000000..1beeba25176b --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_param.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T in method parameter (contravariant position) is rejected +--FILE-- + { + public function take(T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_rw_property.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_rw_property.phpt new file mode 100644 index 000000000000..b03d1aff1fec --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_rw_property.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T in a read/write typed property (invariant position) is rejected +--FILE-- + { + public T $val; +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_set_hook.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_set_hook.phpt new file mode 100644 index 000000000000..28267341e6b5 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_set_hook.phpt @@ -0,0 +1,15 @@ +--TEST-- +Variance: +T in a hooked property's set hook (contravariant position) is rejected +--FILE-- + { + private T $backing; + public function __construct(T $v) { $this->backing = $v; } + public T $val { + get => $this->backing; + set(T $v) { $this->backing = $v; } + } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_sibling_bound.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_sibling_bound.phpt new file mode 100644 index 000000000000..10880152c60f --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_sibling_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T mentioned in a sibling type parameter's bound (invariant position) is rejected +--FILE-- + { public function get(): U; } + +class A<+T, U : Container> {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_through_contravariant_arg_in_return.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_through_contravariant_arg_in_return.phpt new file mode 100644 index 000000000000..ddd4f6825393 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_through_contravariant_arg_in_return.phpt @@ -0,0 +1,12 @@ +--TEST-- +Variance: +T composed through Consumer<-U> in return (+ × - = -) is rejected +--FILE-- + { public function consume(U $x): void; } + +class A<+T> { + public function makeConsumer(): Consumer {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_through_invariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_through_invariant_arg.phpt new file mode 100644 index 000000000000..10005ea1449c --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_through_invariant_arg.phpt @@ -0,0 +1,12 @@ +--TEST-- +Variance: +T composed through an invariant generic's slot (invariant) is rejected +--FILE-- + { public function rw(U $x): U; } + +class A<+T> { + public function get(): Inv {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_extends_contravariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_extends_contravariant_arg.phpt new file mode 100644 index 000000000000..d912f81535d3 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_extends_contravariant_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T as the type-arg to a contravariant parent's parameter is rejected +--FILE-- + {} + +class A<+T> extends Cons {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_extends_invariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_extends_invariant_arg.phpt new file mode 100644 index 000000000000..ac7524f9e28b --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_extends_invariant_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T as the type-arg to an invariant parent's parameter is rejected +--FILE-- + {} + +class A<+T> extends Inv {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_implements_invariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_implements_invariant_arg.phpt new file mode 100644 index 000000000000..f4e63a3f78e9 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_implements_invariant_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T as the type-arg to an invariant interface's parameter is rejected +--FILE-- + {} + +class A<+T> implements Inv {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_line_points_at_method.phpt b/Zend/tests/generics/declaration/variance/violation_line_points_at_method.phpt new file mode 100644 index 000000000000..32b84d702a93 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_line_points_at_method.phpt @@ -0,0 +1,16 @@ +--TEST-- +Variance: a violation inside a method signature reports the method's line, not the class header +--FILE-- + { + public function ok(): T { + throw new RuntimeException(); + } + + public function bad(O $default): T|O { + return $default; + } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line 7 diff --git a/Zend/tests/generics/declaration/variance/violation_method_T_bound_uses_class_T.phpt b/Zend/tests/generics/declaration/variance/violation_method_T_bound_uses_class_T.phpt new file mode 100644 index 000000000000..d1f12650b1cc --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_method_T_bound_uses_class_T.phpt @@ -0,0 +1,12 @@ +--TEST-- +Variance: +T appearing inside a method-level type parameter's bound (invariant) is rejected +--FILE-- + {} + +class A<+T> { + public function map>(W $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_use_trait_invariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_use_trait_invariant_arg.phpt new file mode 100644 index 000000000000..a5b8618683f7 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_use_trait_invariant_arg.phpt @@ -0,0 +1,14 @@ +--TEST-- +Variance: +T as the type-arg to an invariant trait's parameter is rejected +--FILE-- + { + public function rw(U $x): U { return $x; } +} + +class A<+T> { + use InvTrait; +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/erasure/arrow_fn_erasure.phpt b/Zend/tests/generics/erasure/arrow_fn_erasure.phpt new file mode 100644 index 000000000000..d14f301d4cd4 --- /dev/null +++ b/Zend/tests/generics/erasure/arrow_fn_erasure.phpt @@ -0,0 +1,10 @@ +--TEST-- +Erasure: arrow function with type parameters erases to bound +--FILE-- +(T $x): T => $x; +$r = new ReflectionFunction($f); +echo $r->getParameters()[0]->getType()->__toString(), "\n"; +?> +--EXPECT-- +int diff --git a/Zend/tests/generics/erasure/bound_object.phpt b/Zend/tests/generics/erasure/bound_object.phpt new file mode 100644 index 000000000000..815dd1a5e92c --- /dev/null +++ b/Zend/tests/generics/erasure/bound_object.phpt @@ -0,0 +1,12 @@ +--TEST-- +Erasure: T : object erases to object +--FILE-- +(T $x): T { return $x; } +$r = new ReflectionFunction('id'); +echo $r->getParameters()[0]->getType()->__toString(), "\n"; +echo $r->getReturnType()->__toString(), "\n"; +?> +--EXPECT-- +object +object diff --git a/Zend/tests/generics/erasure/bound_to_class.phpt b/Zend/tests/generics/erasure/bound_to_class.phpt new file mode 100644 index 000000000000..7b1cfa50414e --- /dev/null +++ b/Zend/tests/generics/erasure/bound_to_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: T : Foo erases to Foo +--FILE-- +(T $x): T { return $x; } +$r = new ReflectionFunction('id'); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Foo +Foo diff --git a/Zend/tests/generics/erasure/closure_erasure.phpt b/Zend/tests/generics/erasure/closure_erasure.phpt new file mode 100644 index 000000000000..6a533558b3aa --- /dev/null +++ b/Zend/tests/generics/erasure/closure_erasure.phpt @@ -0,0 +1,10 @@ +--TEST-- +Erasure: closure with type parameters erases to bound +--FILE-- +(T $x): T { return $x; }; +$r = new ReflectionFunction($f); +echo $r->getParameters()[0]->getType()->__toString(), "\n"; +?> +--EXPECT-- +object diff --git a/Zend/tests/generics/erasure/composite_bound.phpt b/Zend/tests/generics/erasure/composite_bound.phpt new file mode 100644 index 000000000000..5e38afba9a81 --- /dev/null +++ b/Zend/tests/generics/erasure/composite_bound.phpt @@ -0,0 +1,11 @@ +--TEST-- +Erasure: composite bound erased +--FILE-- +(T $x): T { return $x; } +$rt = (new ReflectionFunction('f'))->getReturnType(); +echo get_class($rt), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/erasure/erased_signature_match.phpt b/Zend/tests/generics/erasure/erased_signature_match.phpt new file mode 100644 index 000000000000..64406feb72a6 --- /dev/null +++ b/Zend/tests/generics/erasure/erased_signature_match.phpt @@ -0,0 +1,20 @@ +--TEST-- +Erasure: generic signature matches hand-erased equivalent +--FILE-- + { + public function get(): T { return new Foo; } +} + +class Erased { + public function get(): Foo { return new Foo; } +} + +$rg = (new ReflectionClass('Generic'))->getMethod('get')->getReturnType()->__toString(); +$re = (new ReflectionClass('Erased'))->getMethod('get')->getReturnType()->__toString(); +var_dump($rg === $re); +?> +--EXPECT-- +bool(true) diff --git a/Zend/tests/generics/erasure/extends_args_discarded.phpt b/Zend/tests/generics/erasure/extends_args_discarded.phpt new file mode 100644 index 000000000000..9fb1c7f59c72 --- /dev/null +++ b/Zend/tests/generics/erasure/extends_args_discarded.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: extends type arguments don't change runtime parent +--FILE-- + {} +class Derived extends Base {} +echo (new ReflectionClass('Derived'))->getParentClass()->getName(), "\n"; +$d = new Derived; +var_dump($d instanceof Base); +?> +--EXPECT-- +Base +bool(true) diff --git a/Zend/tests/generics/erasure/generic_class_param.phpt b/Zend/tests/generics/erasure/generic_class_param.phpt new file mode 100644 index 000000000000..bde3ae6281c5 --- /dev/null +++ b/Zend/tests/generics/erasure/generic_class_param.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: generic class parameter type erased to bound +--FILE-- + { + public function set(T $x): void {} +} +$rm = (new ReflectionClass('Box'))->getMethod('set'); +echo $rm->getParameters()[0]->getType()->getName(), "\n"; +?> +--EXPECT-- +Iface diff --git a/Zend/tests/generics/erasure/get_class_returns_erased.phpt b/Zend/tests/generics/erasure/get_class_returns_erased.phpt new file mode 100644 index 000000000000..51e27f3d54a6 --- /dev/null +++ b/Zend/tests/generics/erasure/get_class_returns_erased.phpt @@ -0,0 +1,10 @@ +--TEST-- +Erasure: get_class returns the erased class name +--FILE-- + {} +$b = new Box::; +echo get_class($b), "\n"; +?> +--EXPECT-- +Box diff --git a/Zend/tests/generics/erasure/instanceof_args_discarded.phpt b/Zend/tests/generics/erasure/instanceof_args_discarded.phpt new file mode 100644 index 000000000000..459815492d66 --- /dev/null +++ b/Zend/tests/generics/erasure/instanceof_args_discarded.phpt @@ -0,0 +1,14 @@ +--TEST-- +Erasure: instanceof type arguments discarded at runtime +--FILE-- +); +var_dump($c instanceof C); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/erasure/intersection_t_bound_class.phpt b/Zend/tests/generics/erasure/intersection_t_bound_class.phpt new file mode 100644 index 000000000000..20421c878257 --- /dev/null +++ b/Zend/tests/generics/erasure/intersection_t_bound_class.phpt @@ -0,0 +1,20 @@ +--TEST-- +Erasure: T in intersection position with class-like bound erases cleanly +--FILE-- +(T & Foo $x): T & Foo { return $x; } + +$r = new ReflectionFunction('pickIface'); +echo "param: ", $r->getParameters()[0]->getType(), "\n"; +echo "return: ", $r->getReturnType(), "\n"; + +class BarFoo implements Bar, Foo {} +var_dump(pickIface(new BarFoo) instanceof BarFoo); +?> +--EXPECT-- +param: Bar&Foo +return: Bar&Foo +bool(true) diff --git a/Zend/tests/generics/erasure/method_bound.phpt b/Zend/tests/generics/erasure/method_bound.phpt new file mode 100644 index 000000000000..4562b258efad --- /dev/null +++ b/Zend/tests/generics/erasure/method_bound.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: method-level type parameter erased +--FILE-- +(U $x): U { return $x; } +} +$rt = (new ReflectionClass('C'))->getMethod('m')->getReturnType(); +echo $rt->getName(), "\n"; +?> +--EXPECT-- +Stringable2 diff --git a/Zend/tests/generics/erasure/method_inheritance_erased.phpt b/Zend/tests/generics/erasure/method_inheritance_erased.phpt new file mode 100644 index 000000000000..a94f25177bba --- /dev/null +++ b/Zend/tests/generics/erasure/method_inheritance_erased.phpt @@ -0,0 +1,19 @@ +--TEST-- +Erasure: child overriding generic parent method works on erased signature +--FILE-- +(T $x): T { return $x; } +} +class Child extends Parent2 { + public function f(U $x): U { return $x; } +} +$pm = (new ReflectionClass('Parent2'))->getMethod('f'); +$cm = (new ReflectionClass('Child'))->getMethod('f'); +echo $pm->getParameters()[0]->getType()->getName(), "\n"; +echo $cm->getParameters()[0]->getType()->getName(), "\n"; +?> +--EXPECT-- +Animal +Animal diff --git a/Zend/tests/generics/erasure/named_args_stripped.phpt b/Zend/tests/generics/erasure/named_args_stripped.phpt new file mode 100644 index 000000000000..437580417c71 --- /dev/null +++ b/Zend/tests/generics/erasure/named_args_stripped.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: type arguments stripped from named types in runtime view +--FILE-- + $x): Container { return $x; } +$r = new ReflectionFunction('f'); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Container +Container diff --git a/Zend/tests/generics/erasure/property_type_erased.phpt b/Zend/tests/generics/erasure/property_type_erased.phpt new file mode 100644 index 000000000000..9b1e93e3d5a5 --- /dev/null +++ b/Zend/tests/generics/erasure/property_type_erased.phpt @@ -0,0 +1,16 @@ +--TEST-- +Erasure: generic property type erased to bound +--FILE-- + { + public T $value; +} +$rp = (new ReflectionClass('Box'))->getProperty('value'); +$rt = $rp->getType(); +echo get_class($rt), "\n"; +echo $rt->getName(), "\n"; +?> +--EXPECT-- +ReflectionNamedType +Foo diff --git a/Zend/tests/generics/erasure/runtime_bound_check.phpt b/Zend/tests/generics/erasure/runtime_bound_check.phpt new file mode 100644 index 000000000000..dc2b563701ec --- /dev/null +++ b/Zend/tests/generics/erasure/runtime_bound_check.phpt @@ -0,0 +1,21 @@ +--TEST-- +Erasure: bound is enforced at runtime +--FILE-- +(T $x): T { return $x; } + +echo get_class(f(new Dog)), "\n"; + +try { + f("not an animal"); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "caught\n"; +} +?> +--EXPECT-- +Dog +caught diff --git a/Zend/tests/generics/erasure/runtime_unbounded.phpt b/Zend/tests/generics/erasure/runtime_unbounded.phpt new file mode 100644 index 000000000000..e86b52e60926 --- /dev/null +++ b/Zend/tests/generics/erasure/runtime_unbounded.phpt @@ -0,0 +1,20 @@ +--TEST-- +Erasure: unbounded T accepts anything (mixed) +--FILE-- +(T $x): T { return $x; } +var_dump(id(42)); +var_dump(id("hello")); +var_dump(id(null)); +var_dump(id([1, 2])); +?> +--EXPECT-- +int(42) +string(5) "hello" +NULL +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} diff --git a/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt b/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt new file mode 100644 index 000000000000..10456f3cfe9f --- /dev/null +++ b/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt @@ -0,0 +1,72 @@ +--TEST-- +Erasure: T inside nullable and union types collapses redundant nulls/mixed +--FILE-- +(?T $x): ?T { return $x; } +function nullableBounded(?T $x): ?T { return $x; } +function unionNullUnbounded(null|T $x): null|T { return $x; } +function unionMultiUnbounded(int|bool|T $x): int|bool|T { return $x; } +function unionNullBounded(null|T $x): null|T { return $x; } +function unionMultiBounded(int|T $x): int|T { return $x; } + +class Holder { + public function __construct(public ?T $value) {} + public function set(?T $v): void { $this->value = $v; } + public function get(): ?T { return $this->value; } +} + +var_dump(nullableUnbounded(42)); +var_dump(nullableUnbounded(null)); +var_dump(nullableUnbounded("hi")); +var_dump(nullableBounded(7)); +var_dump(nullableBounded(null)); +var_dump(unionNullUnbounded(null)); +var_dump(unionNullUnbounded("x")); +var_dump(unionMultiUnbounded(1)); +var_dump(unionMultiUnbounded(true)); +var_dump(unionMultiUnbounded(new stdClass)); +var_dump(unionNullBounded(null)); +var_dump(unionNullBounded(7)); +var_dump(unionMultiBounded(1)); + +echo "nullableUnbounded: ", (new ReflectionFunction('nullableUnbounded'))->getReturnType(), "\n"; +echo "nullableBounded: ", (new ReflectionFunction('nullableBounded'))->getReturnType(), "\n"; +echo "unionNullUnbounded: ", (new ReflectionFunction('unionNullUnbounded'))->getReturnType(), "\n"; +echo "unionMultiUnbounded: ", (new ReflectionFunction('unionMultiUnbounded'))->getReturnType(), "\n"; +echo "unionNullBounded: ", (new ReflectionFunction('unionNullBounded'))->getReturnType(), "\n"; +echo "unionMultiBounded: ", (new ReflectionFunction('unionMultiBounded'))->getReturnType(), "\n"; + +$rc = new ReflectionClass(Holder::class); +echo "Holder property: ", $rc->getProperty('value')->getType(), "\n"; +echo "Holder set param: ", $rc->getMethod('set')->getParameters()[0]->getType(), "\n"; +echo "Holder get return: ", $rc->getMethod('get')->getReturnType(), "\n"; + +$h = new Holder(1); +$h->set(null); +var_dump($h->get()); +?> +--EXPECT-- +int(42) +NULL +string(2) "hi" +int(7) +NULL +NULL +string(1) "x" +int(1) +bool(true) +object(stdClass)#1 (0) { +} +NULL +int(7) +int(1) +nullableUnbounded: mixed +nullableBounded: ?int +unionNullUnbounded: mixed +unionMultiUnbounded: mixed +unionNullBounded: ?int +unionMultiBounded: Stringable|int +Holder property: mixed +Holder set param: mixed +Holder get return: mixed +NULL diff --git a/Zend/tests/generics/erasure/turbofish_no_runtime_effect.phpt b/Zend/tests/generics/erasure/turbofish_no_runtime_effect.phpt new file mode 100644 index 000000000000..a41ecae4ace3 --- /dev/null +++ b/Zend/tests/generics/erasure/turbofish_no_runtime_effect.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: turbofish (with matching arity) has zero runtime effect +--FILE-- +($x) { return $x * 2; } +var_dump(f(5)); +var_dump(f::(5)); +var_dump(f::(5)); +?> +--EXPECT-- +int(10) +int(10) +int(10) diff --git a/Zend/tests/generics/erasure/unbounded_to_mixed.phpt b/Zend/tests/generics/erasure/unbounded_to_mixed.phpt new file mode 100644 index 000000000000..15ff253c3173 --- /dev/null +++ b/Zend/tests/generics/erasure/unbounded_to_mixed.phpt @@ -0,0 +1,12 @@ +--TEST-- +Erasure: unbounded T erases to mixed +--FILE-- +(T $x): T { return $x; } +$r = new ReflectionFunction('id'); +echo $r->getParameters()[0]->getType()->__toString(), "\n"; +echo $r->getReturnType()->__toString(), "\n"; +?> +--EXPECT-- +mixed +mixed diff --git a/Zend/tests/generics/erasure/var_dump_class_name.phpt b/Zend/tests/generics/erasure/var_dump_class_name.phpt new file mode 100644 index 000000000000..c852fe9d88ee --- /dev/null +++ b/Zend/tests/generics/erasure/var_dump_class_name.phpt @@ -0,0 +1,14 @@ +--TEST-- +Erasure: var_dump shows erased class name +--FILE-- + { + public int $x = 1; +} +var_dump(new Box::); +?> +--EXPECT-- +object(Box)#1 (1) { + ["x"]=> + int(1) +} diff --git a/Zend/tests/generics/errors/anonymous_class_no_params.phpt b/Zend/tests/generics/errors/anonymous_class_no_params.phpt new file mode 100644 index 000000000000..6aca672cb9b2 --- /dev/null +++ b/Zend/tests/generics/errors/anonymous_class_no_params.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: anonymous classes cannot declare type parameters +--FILE-- + {};'); +} catch (ParseError $e) { + echo "parse error: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +parse error: %s diff --git a/Zend/tests/generics/errors/closure_shadows_function.phpt b/Zend/tests/generics/errors/closure_shadows_function.phpt new file mode 100644 index 000000000000..29c6c8397605 --- /dev/null +++ b/Zend/tests/generics/errors/closure_shadows_function.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: closure type parameter shadows enclosing function type parameter +--FILE-- +(): void { + $cl = function (): void {}; +} +?> +--EXPECTF-- +Fatal error: Type parameter T shadows enclosing type parameter in %s on line %d diff --git a/Zend/tests/generics/errors/duplicate_in_method.phpt b/Zend/tests/generics/errors/duplicate_in_method.phpt new file mode 100644 index 000000000000..78fd038e233c --- /dev/null +++ b/Zend/tests/generics/errors/duplicate_in_method.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: duplicate type parameter inside a method's own list +--FILE-- +(): void {} } +?> +--EXPECTF-- +Fatal error: Cannot redeclare type parameter T in %s on line %d diff --git a/Zend/tests/generics/errors/duplicate_type_param.phpt b/Zend/tests/generics/errors/duplicate_type_param.phpt new file mode 100644 index 000000000000..77c148794173 --- /dev/null +++ b/Zend/tests/generics/errors/duplicate_type_param.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: duplicate type parameter name +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Cannot redeclare type parameter T in %s on line %d diff --git a/Zend/tests/generics/errors/duplicate_type_param_function.phpt b/Zend/tests/generics/errors/duplicate_type_param_function.phpt new file mode 100644 index 000000000000..f9ad4e8b270b --- /dev/null +++ b/Zend/tests/generics/errors/duplicate_type_param_function.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: duplicate type parameter name on function +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Cannot redeclare type parameter U in %s on line %d diff --git a/Zend/tests/generics/errors/empty_arg_list.phpt b/Zend/tests/generics/errors/empty_arg_list.phpt new file mode 100644 index 000000000000..6ac10dcff2db --- /dev/null +++ b/Zend/tests/generics/errors/empty_arg_list.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: empty type argument list +--FILE-- + $x): void {}'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/empty_param_list.phpt b/Zend/tests/generics/errors/empty_param_list.phpt new file mode 100644 index 000000000000..918533b25f26 --- /dev/null +++ b/Zend/tests/generics/errors/empty_param_list.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: empty type parameter list +--FILE-- + {}'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/forward_ref_at_self.phpt b/Zend/tests/generics/errors/forward_ref_at_self.phpt new file mode 100644 index 000000000000..b4dd1601bd0a --- /dev/null +++ b/Zend/tests/generics/errors/forward_ref_at_self.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: bound `T : T` (top-level self-reference) is rejected +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/errors/forward_reference_in_default.phpt b/Zend/tests/generics/errors/forward_reference_in_default.phpt new file mode 100644 index 000000000000..915c0b4d09dd --- /dev/null +++ b/Zend/tests/generics/errors/forward_reference_in_default.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: forward reference in default of earlier parameter +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type parameter T referenced before declaration in %s on line %d diff --git a/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt b/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt new file mode 100644 index 000000000000..4ce03e67f0dd --- /dev/null +++ b/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt @@ -0,0 +1,9 @@ +--TEST-- +Errors: T in intersection position with scalar bound +--FILE-- +(): T & Foo {} +?> +--EXPECTF-- +Fatal error: Type parameter T with bound string cannot be part of an intersection type; use an object-shaped bound (e.g. T: object) in %s on line %d diff --git a/Zend/tests/generics/errors/intersection_t_unbounded.phpt b/Zend/tests/generics/errors/intersection_t_unbounded.phpt new file mode 100644 index 000000000000..940b218d2e5c --- /dev/null +++ b/Zend/tests/generics/errors/intersection_t_unbounded.phpt @@ -0,0 +1,9 @@ +--TEST-- +Errors: T in intersection position with unbounded T +--FILE-- +(): T & Foo {} +?> +--EXPECTF-- +Fatal error: Type parameter T with bound mixed cannot be part of an intersection type; use an object-shaped bound (e.g. T: object) in %s on line %d diff --git a/Zend/tests/generics/errors/method_shadows_class_param.phpt b/Zend/tests/generics/errors/method_shadows_class_param.phpt new file mode 100644 index 000000000000..6c8b3cfd7935 --- /dev/null +++ b/Zend/tests/generics/errors/method_shadows_class_param.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: method type parameter shadows class type parameter +--FILE-- + { + public function f(): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T shadows enclosing type parameter in %s on line %d diff --git a/Zend/tests/generics/errors/missing_close_angle.phpt b/Zend/tests/generics/errors/missing_close_angle.phpt new file mode 100644 index 000000000000..0ee4bae3581f --- /dev/null +++ b/Zend/tests/generics/errors/missing_close_angle.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: missing closing angle in type parameter list +--FILE-- + +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/missing_param_after_comma.phpt b/Zend/tests/generics/errors/missing_param_after_comma.phpt new file mode 100644 index 000000000000..df223a838c29 --- /dev/null +++ b/Zend/tests/generics/errors/missing_param_after_comma.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: missing parameter after trailing comma plus comma +--FILE-- + {}'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/optional_only_at_tail_ok.phpt b/Zend/tests/generics/errors/optional_only_at_tail_ok.phpt new file mode 100644 index 000000000000..101a3f0c6f85 --- /dev/null +++ b/Zend/tests/generics/errors/optional_only_at_tail_ok.phpt @@ -0,0 +1,15 @@ +--TEST-- +Errors: optional type parameters at the tail are accepted +--FILE-- + {} +class B {} +class C {} +class D {} +class E {} +function f(): void {} +function g(): void {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/errors/recursive_bound_ok.phpt b/Zend/tests/generics/errors/recursive_bound_ok.phpt new file mode 100644 index 000000000000..2b05466e6096 --- /dev/null +++ b/Zend/tests/generics/errors/recursive_bound_ok.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: recursive bound (T : Comparable) is allowed (positive test) +--FILE-- + {} +function f>(T $x): T { return $x; } +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/errors/required_after_optional.phpt b/Zend/tests/generics/errors/required_after_optional.phpt new file mode 100644 index 000000000000..65a48dad1157 --- /dev/null +++ b/Zend/tests/generics/errors/required_after_optional.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: required type parameter cannot follow an optional one +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Optional type parameter T cannot be declared before required type parameter U in %s on line %d diff --git a/Zend/tests/generics/errors/required_after_optional_function.phpt b/Zend/tests/generics/errors/required_after_optional_function.phpt new file mode 100644 index 000000000000..3bfefa731931 --- /dev/null +++ b/Zend/tests/generics/errors/required_after_optional_function.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: required type parameter cannot follow an optional one +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Optional type parameter T cannot be declared before required type parameter U in %s on line %d diff --git a/Zend/tests/generics/errors/required_after_optional_three_params.phpt b/Zend/tests/generics/errors/required_after_optional_three_params.phpt new file mode 100644 index 000000000000..a4a5ad579ace --- /dev/null +++ b/Zend/tests/generics/errors/required_after_optional_three_params.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: required type parameter following two optional ones names the first optional +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Optional type parameter T cannot be declared before required type parameter V in %s on line %d diff --git a/Zend/tests/generics/errors/runtime_bound_violation.phpt b/Zend/tests/generics/errors/runtime_bound_violation.phpt new file mode 100644 index 000000000000..c6589429f414 --- /dev/null +++ b/Zend/tests/generics/errors/runtime_bound_violation.phpt @@ -0,0 +1,16 @@ +--TEST-- +Errors: runtime type error when bound is violated +--FILE-- +(T $x): T { return $x; } + +try { + f(42); + echo "no error\n"; +} catch (TypeError $e) { + echo "type error\n"; +} +?> +--EXPECT-- +type error diff --git a/Zend/tests/generics/errors/runtime_unbound_accepts_anything.phpt b/Zend/tests/generics/errors/runtime_unbound_accepts_anything.phpt new file mode 100644 index 000000000000..23084a77f8dd --- /dev/null +++ b/Zend/tests/generics/errors/runtime_unbound_accepts_anything.phpt @@ -0,0 +1,19 @@ +--TEST-- +Errors: runtime accepts anything for unbounded T (mixed) +--FILE-- +(T $x): T { return $x; } + +var_dump(f(42)); +var_dump(f("foo")); +var_dump(f(null)); +var_dump(f(new stdClass)); +echo "no errors\n"; +?> +--EXPECT-- +int(42) +string(3) "foo" +NULL +object(stdClass)#1 (0) { +} +no errors diff --git a/Zend/tests/generics/errors/self_ref_in_default.phpt b/Zend/tests/generics/errors/self_ref_in_default.phpt new file mode 100644 index 000000000000..d852a85d83a5 --- /dev/null +++ b/Zend/tests/generics/errors/self_ref_in_default.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: top-level self-reference in default is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/errors/self_ref_in_intersection.phpt b/Zend/tests/generics/errors/self_ref_in_intersection.phpt new file mode 100644 index 000000000000..3a6970bf3a4a --- /dev/null +++ b/Zend/tests/generics/errors/self_ref_in_intersection.phpt @@ -0,0 +1,9 @@ +--TEST-- +Errors: top-level self-reference inside an intersection bound is rejected +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/errors/self_ref_in_union.phpt b/Zend/tests/generics/errors/self_ref_in_union.phpt new file mode 100644 index 000000000000..e40264a7b61d --- /dev/null +++ b/Zend/tests/generics/errors/self_ref_in_union.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: top-level self-reference inside a union bound is rejected +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/errors/turbofish_no_call_target.phpt b/Zend/tests/generics/errors/turbofish_no_call_target.phpt new file mode 100644 index 000000000000..fd3c7fb8be6c --- /dev/null +++ b/Zend/tests/generics/errors/turbofish_no_call_target.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: bare turbofish without function call +--FILE-- +;'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/turbofish_with_space.phpt b/Zend/tests/generics/errors/turbofish_with_space.phpt new file mode 100644 index 000000000000..a7460772979a --- /dev/null +++ b/Zend/tests/generics/errors/turbofish_with_space.phpt @@ -0,0 +1,13 @@ +--TEST-- +Errors: whitespace between :: and < is not turbofish +--FILE-- +::f();'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/type_args_in_class_const.phpt b/Zend/tests/generics/errors/type_args_in_class_const.phpt new file mode 100644 index 000000000000..8077a89c3e38 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_in_class_const.phpt @@ -0,0 +1,13 @@ +--TEST-- +Errors: type arguments not allowed in expression position (Foo::class) +--FILE-- +::class;'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/type_args_in_static_call.phpt b/Zend/tests/generics/errors/type_args_in_static_call.phpt new file mode 100644 index 000000000000..509cd35991a2 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_in_static_call.phpt @@ -0,0 +1,13 @@ +--TEST-- +Errors: type arguments not allowed before :: (use ::<...> turbofish instead) +--FILE-- +::bar();'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/type_param_in_catch.phpt b/Zend/tests/generics/errors/type_param_in_catch.phpt new file mode 100644 index 000000000000..9141899cb0f9 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_catch.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: type parameter cannot be used at runtime in `catch` +--FILE-- +(): void { + try { /* ... */ } catch (T $e) {} +} +?> +--EXPECTF-- +Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_class_const.phpt b/Zend/tests/generics/errors/type_param_in_class_const.phpt new file mode 100644 index 000000000000..7db77b73d862 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_class_const.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: type parameter cannot be used at runtime in `T::class` +--FILE-- +(): string { + return T::class; +} +?> +--EXPECTF-- +Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_instanceof.phpt b/Zend/tests/generics/errors/type_param_in_instanceof.phpt new file mode 100644 index 000000000000..34b51546cda1 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_instanceof.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: type parameter cannot be used at runtime in `instanceof` +--FILE-- +($x): bool { + return $x instanceof T; +} +?> +--EXPECTF-- +Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_new_expression.phpt b/Zend/tests/generics/errors/type_param_in_new_expression.phpt new file mode 100644 index 000000000000..f10f9ace66e0 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_new_expression.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: type parameter cannot be used at runtime as a class in `new T()` +--FILE-- +(): void { + $x = new T(); +} +?> +--EXPECTF-- +Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_call.phpt b/Zend/tests/generics/errors/type_param_in_static_call.phpt new file mode 100644 index 000000000000..ed23a33db626 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_call.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: type parameter cannot be used at runtime as a static-call target +--FILE-- +(): void { + T::foo(); +} +?> +--EXPECTF-- +Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method.phpt b/Zend/tests/generics/errors/type_param_in_static_method.phpt new file mode 100644 index 000000000000..aa67becd88c9 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used in a static method signature +--FILE-- + { + public static function foo(T $a): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_bound.phpt b/Zend/tests/generics/errors/type_param_in_static_method_bound.phpt new file mode 100644 index 000000000000..ebb09bf3ddb4 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used as a bound on a static method's own type parameter +--FILE-- + { + public static function foo(U $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_default.phpt b/Zend/tests/generics/errors/type_param_in_static_method_default.phpt new file mode 100644 index 000000000000..a2d5edbb94fa --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_default.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used as a default for a static method's own type parameter +--FILE-- + { + public static function foo(U $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_in_trait.phpt b/Zend/tests/generics/errors/type_param_in_static_method_in_trait.phpt new file mode 100644 index 000000000000..bff9674b4951 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_in_trait.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: trait type parameter cannot be used in a static method signature +--FILE-- + { + public static function make(T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to Bag and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_intersection.phpt b/Zend/tests/generics/errors/type_param_in_static_method_intersection.phpt new file mode 100644 index 000000000000..afe7ee4a36c1 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_intersection.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot appear in an intersection type on a static method +--FILE-- + { + public static function foo(T&Countable $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_nested.phpt b/Zend/tests/generics/errors/type_param_in_static_method_nested.phpt new file mode 100644 index 000000000000..2c467f43333d --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_nested.phpt @@ -0,0 +1,14 @@ +--TEST-- +Errors: class type parameter cannot be nested inside a generic type in a static method signature +--FILE-- + { + public function __construct(public null|U $value) {} +} + +class Example { + public static function unwrap(Optional $value): mixed { return $value->value; } +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to Example and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_nullable.phpt b/Zend/tests/generics/errors/type_param_in_static_method_nullable.phpt new file mode 100644 index 000000000000..a021dd2ee9e4 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_nullable.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot appear in a nullable type on a static method +--FILE-- + { + public static function foo(?T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_return.phpt b/Zend/tests/generics/errors/type_param_in_static_method_return.phpt new file mode 100644 index 000000000000..bbcd3b326094 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_return.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used as a static method return type +--FILE-- + { + public static function foo(): T {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_variadic.phpt b/Zend/tests/generics/errors/type_param_in_static_method_variadic.phpt new file mode 100644 index 000000000000..ea2297380515 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_variadic.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used in a variadic static method parameter +--FILE-- + { + public static function foo(T ...$xs): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_property.phpt b/Zend/tests/generics/errors/type_param_in_static_property.phpt new file mode 100644 index 000000000000..55429fe1de8c --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_property.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used in a static property type +--FILE-- + { + public static null|T $a = null; +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_property_in_trait.phpt b/Zend/tests/generics/errors/type_param_in_static_property_in_trait.phpt new file mode 100644 index 000000000000..ba1ba26c80c3 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_property_in_trait.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: trait type parameter cannot be used as a static property type +--FILE-- + { + public static null|T $slot = null; +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to Bag and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_property_nested.phpt b/Zend/tests/generics/errors/type_param_in_static_property_nested.phpt new file mode 100644 index 000000000000..a0bfbc78dd30 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_property_nested.phpt @@ -0,0 +1,14 @@ +--TEST-- +Errors: class type parameter cannot be nested inside a generic type on a static property +--FILE-- + { + public function __construct(public U $value) {} +} + +class A { + public static ?Container $box = null; +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_no_args_required.phpt b/Zend/tests/generics/inheritance/arity/extends_no_args_required.phpt new file mode 100644 index 000000000000..5e7e4c88737d --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_no_args_required.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends without args, parent has required type parameter, is a compile error +--FILE-- + {} +class Bad extends Box {} +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to extends Box in Bad, 0 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt b/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt new file mode 100644 index 000000000000..c4d213c2c4d4 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends a non-generic class with type arguments is a compile error +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to extends Plain in Bad, 1 passed and exactly 0 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_over_default_max.phpt b/Zend/tests/generics/inheritance/arity/extends_over_default_max.phpt new file mode 100644 index 000000000000..ed07eb160fbf --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_over_default_max.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends Pair with 3 args is a compile error +--FILE-- + {} +class Bad extends Pair {} +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to extends Pair in Bad, 3 passed and at most 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_too_few.phpt b/Zend/tests/generics/inheritance/arity/extends_too_few.phpt new file mode 100644 index 000000000000..ca15febd2da7 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_too_few.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends with too few type arguments is a compile error +--FILE-- + {} +class Bad extends Pair {} +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to extends Pair in Bad, 1 passed and exactly 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_too_many.phpt b/Zend/tests/generics/inheritance/arity/extends_too_many.phpt new file mode 100644 index 000000000000..d8e7cdc124ba --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_too_many.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends with too many type arguments is a compile error +--FILE-- + {} +class Bad extends Box {} +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to extends Box in Bad, 2 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/implements_no_args_required.phpt b/Zend/tests/generics/inheritance/arity/implements_no_args_required.phpt new file mode 100644 index 000000000000..49ef6bdcffb2 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/implements_no_args_required.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: implements without args, interface has required type parameter, is a compile error +--FILE-- + {} +class Bad implements I {} +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to implements I in Bad, 0 passed and exactly 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/implements_too_few.phpt b/Zend/tests/generics/inheritance/arity/implements_too_few.phpt new file mode 100644 index 000000000000..1902c7df1a99 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/implements_too_few.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: implements with too few type arguments is a compile error +--FILE-- + {} +class Bad implements Map {} +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to implements Map in Bad, 1 passed and exactly 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/interface_extends_arity.phpt b/Zend/tests/generics/inheritance/arity/interface_extends_arity.phpt new file mode 100644 index 000000000000..1ebd7e7586e1 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/interface_extends_arity.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: interface extends another interface with wrong arity is a compile error +--FILE-- + {} +interface IExt extends J {} +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to extends J in IExt, 2 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/no_args_with_defaults.phpt b/Zend/tests/generics/inheritance/arity/no_args_with_defaults.phpt new file mode 100644 index 000000000000..3c0019575e98 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/no_args_with_defaults.phpt @@ -0,0 +1,16 @@ +--TEST-- +Inheritance arity: no args is OK when all type parameters have defaults +--FILE-- + {} +interface I {} +trait Holder { public T $v; } + +class A extends Box {} +class B implements I {} +class C { use Holder; } + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/arity/use_trait_no_args_required.phpt b/Zend/tests/generics/inheritance/arity/use_trait_no_args_required.phpt new file mode 100644 index 000000000000..dbaed739f0cf --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/use_trait_no_args_required.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: use Trait without args, trait has required type parameter, is a compile error +--FILE-- + { public T $val; } +class Bad { use Holder; } +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to use Holder in Bad, 0 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/use_trait_too_many.phpt b/Zend/tests/generics/inheritance/arity/use_trait_too_many.phpt new file mode 100644 index 000000000000..1c9f085a4074 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/use_trait_too_many.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: use trait with too many type arguments is a compile error +--FILE-- + { public T $v; } +class Bad { use Holder; } +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to use Holder in Bad, 2 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/with_defaults.phpt b/Zend/tests/generics/inheritance/arity/with_defaults.phpt new file mode 100644 index 000000000000..7420fbe70c36 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/with_defaults.phpt @@ -0,0 +1,19 @@ +--TEST-- +Inheritance arity: defaults extend the accepted range +--FILE-- + {} +interface Maybe {} +trait Slot {} + +// Required-only arity +class P1 extends Pair {} +// Including the optional +class P2 extends Pair {} +// Trait with default — works without args +class T1 { use Slot; } + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/bounds/bound_on_bound_pass.phpt b/Zend/tests/generics/inheritance/bounds/bound_on_bound_pass.phpt new file mode 100644 index 000000000000..260e17395710 --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/bound_on_bound_pass.phpt @@ -0,0 +1,18 @@ +--TEST-- +Bound check: forwarded T-ref where child's bound is at least as tight as parent's +--FILE-- + {} +interface B extends A {} +interface C extends A {} + +class IntBox {} +class IntSubBox extends IntBox {} + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/bounds/bound_on_bound_too_loose.phpt b/Zend/tests/generics/inheritance/bounds/bound_on_bound_too_loose.phpt new file mode 100644 index 000000000000..714911b933fa --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/bound_on_bound_too_loose.phpt @@ -0,0 +1,12 @@ +--TEST-- +Bound check: forwarded T-ref where child's bound is looser than parent's is rejected +--FILE-- + {} +interface B extends A {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to extends A in B does not satisfy the bound Dog on parameter T, Y given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/bound_on_bound_unbounded_arg.phpt b/Zend/tests/generics/inheritance/bounds/bound_on_bound_unbounded_arg.phpt new file mode 100644 index 000000000000..be93b443b778 --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/bound_on_bound_unbounded_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Bound check: forwarded T-ref where child has no bound is rejected when parent requires one +--FILE-- + {} +interface B extends A {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to extends A in B does not satisfy the bound Animal on parameter T, W given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/error_renders_args.phpt b/Zend/tests/generics/inheritance/bounds/error_renders_args.phpt new file mode 100644 index 000000000000..dd539770f094 --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/error_renders_args.phpt @@ -0,0 +1,12 @@ +--TEST-- +Bound error message: link-time bound violation renders NAMED_WITH_ARGS with its type arguments +--FILE-- + {} + +interface Bag> {} + +class IntBag implements Bag {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to implements Bag in IntBag does not satisfy the bound Comparable on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/extends_concrete_satisfies.phpt b/Zend/tests/generics/inheritance/bounds/extends_concrete_satisfies.phpt new file mode 100644 index 000000000000..aa1517ccc37f --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/extends_concrete_satisfies.phpt @@ -0,0 +1,13 @@ +--TEST-- +Bound check: class extends with a concrete arg satisfying the parent bound +--FILE-- + {} + +class DogBox extends Box {} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/bounds/extends_violates.phpt b/Zend/tests/generics/inheritance/bounds/extends_violates.phpt new file mode 100644 index 000000000000..568a47a0f779 --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/extends_violates.phpt @@ -0,0 +1,11 @@ +--TEST-- +Bound check: class extends with a concrete arg violating the parent bound +--FILE-- + {} + +class IntBox extends Box {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to extends Box in IntBox does not satisfy the bound Animal on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/implements_violates.phpt b/Zend/tests/generics/inheritance/bounds/implements_violates.phpt new file mode 100644 index 000000000000..ce909c2fa42f --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/implements_violates.phpt @@ -0,0 +1,11 @@ +--TEST-- +Bound check: implements with a concrete arg violating the interface bound +--FILE-- + {} + +class IntBag implements Bag {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to implements Bag in IntBag does not satisfy the bound Animal on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/interface_extends_interface_violates.phpt b/Zend/tests/generics/inheritance/bounds/interface_extends_interface_violates.phpt new file mode 100644 index 000000000000..34910ac0040d --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/interface_extends_interface_violates.phpt @@ -0,0 +1,11 @@ +--TEST-- +Bound check: interface extends another generic interface with violating arg +--FILE-- + {} + +interface B extends A {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to extends A in B does not satisfy the bound Animal on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/use_trait_violates.phpt b/Zend/tests/generics/inheritance/bounds/use_trait_violates.phpt new file mode 100644 index 000000000000..9381fbcc40fd --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/use_trait_violates.phpt @@ -0,0 +1,11 @@ +--TEST-- +Bound check: use trait with a concrete arg violating the trait bound +--FILE-- + { public T $val; } + +class IntHolder { use Holder; } +?> +--EXPECTF-- +Fatal error: Type argument 1 to use Holder in IntHolder does not satisfy the bound Animal on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/conflicting_parent_and_interface.phpt b/Zend/tests/generics/inheritance/diamond/conflicting_parent_and_interface.phpt new file mode 100644 index 000000000000..3f520348eb81 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/conflicting_parent_and_interface.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond: parent and direct interface providing different bindings for the same invariant ancestor (class case is deferred to per-path LSP) +--FILE-- + {} +class P implements Box {} + +class C extends P implements Box {} +$c = new C(); +var_dump($c instanceof Box, $c instanceof P); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/conflicting_via_two_interfaces.phpt b/Zend/tests/generics/inheritance/diamond/conflicting_via_two_interfaces.phpt new file mode 100644 index 000000000000..219ae6c215a9 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/conflicting_via_two_interfaces.phpt @@ -0,0 +1,16 @@ +--TEST-- +Diamond: class implementing two interfaces that bind the same invariant ancestor (accepted, LSP per path validates any actual methods) +--FILE-- + {} +interface ABox extends Box {} +interface BBox extends Box {} + +class C implements ABox, BBox {} +$c = new C(); +var_dump($c instanceof Box, $c instanceof ABox, $c instanceof BBox); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/forwarding_t_consistent.phpt b/Zend/tests/generics/inheritance/diamond/forwarding_t_consistent.phpt new file mode 100644 index 000000000000..bc413328885c --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/forwarding_t_consistent.phpt @@ -0,0 +1,12 @@ +--TEST-- +Diamond: generic class forwarding the same T into two paths is accepted +--FILE-- + {} +interface Wrapper extends Box {} + +class C implements Wrapper, Box {} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt b/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt new file mode 100644 index 000000000000..f0887f4b7523 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond: a generic class forwarding T conflicts with a concrete arg on another path (accepted for the class form, LSP catches actual signature mismatches) +--FILE-- + {} +interface Wrapper extends Box {} + +class C implements Wrapper, Box {} +$c = new C(); +var_dump($c instanceof Box, $c instanceof Wrapper); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_covariant_missing.phpt b/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_covariant_missing.phpt new file mode 100644 index 000000000000..00cefe61d12e --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_covariant_missing.phpt @@ -0,0 +1,17 @@ +--TEST-- +Diamond + variance: class implementing a covariant interface diamond must return the intersection +--FILE-- + { public function get(): X; } +interface Flex extends Getter, Getter {} + +class Bad implements Flex { + public function get(): JustFoo { return new JustFoo(); } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::get(): JustFoo must be compatible with %s::get(): IFoo&IBar in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_missing.phpt b/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_missing.phpt new file mode 100644 index 000000000000..cbd41974f4dd --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_missing.phpt @@ -0,0 +1,13 @@ +--TEST-- +Diamond + variance: class implementing an interface diamond must satisfy the merged signature +--FILE-- + { public function set(X $v): void; } +interface Flex extends Setter, Setter {} + +class Bad implements Flex { + public function set(int $v): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::set(int $v): void must be compatible with %s::set(string|int $v): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/interface_diamond_variance.phpt b/Zend/tests/generics/inheritance/diamond/interface_diamond_variance.phpt new file mode 100644 index 000000000000..4405f1b4a691 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/interface_diamond_variance.phpt @@ -0,0 +1,121 @@ +--TEST-- +Diamond + variance: an interface can itself diamond-inherit a generic ancestor +--FILE-- + { public function set(X $v): void; } +interface I1IntSink extends I1Setter {} +interface I1StrSink extends I1Setter {} +interface I1Flex extends I1IntSink, I1StrSink {} + +final class I1Impl implements I1Flex { + public function set(int|string $v): void { echo "I1: ", var_export($v, true), "\n"; } +} + +interface I2Setter<-X> { public function set(X $v): void; } +interface I2Flex extends I2Setter, I2Setter {} + +final class I2Impl implements I2Flex { + public function set(int|string $v): void { echo "I2: ", var_export($v, true), "\n"; } +} + +interface I3Setter<-X> { public function set(X $v): void; } +interface I3Flex extends I3Setter, I3Setter, I3Setter {} + +final class I3Impl implements I3Flex { + public function set(int|string|float $v): void { echo "I3: ", var_export($v, true), "\n"; } +} + +interface I4Getter<+X : object> { public function get(): X; } +interface I4FooSrc extends I4Getter {} +interface I4BarSrc extends I4Getter {} +interface I4Flex extends I4FooSrc, I4BarSrc {} + +final class I4Impl implements I4Flex { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface I5Getter<+X : object> { public function get(): X; } +interface I5Flex extends I5Getter, I5Getter, I5Getter {} + +final class I5Impl implements I5Flex { + public function get(): IFoo&IBar&IBaz { return new FooBarBaz(); } +} + +interface I6IO<+X : object, -Y> { + public function get(): X; + public function set(Y $v): void; +} +interface I6Flex extends I6IO, I6IO {} + +final class I6Impl implements I6Flex { + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "I6.set: ", var_export($v, true), "\n"; } +} + +interface I7Setter<-X> { public function set(X $v): void; } +interface I7L1A extends I7Setter {} +interface I7L2A extends I7L1A {} +interface I7L1B extends I7Setter {} +interface I7L2B extends I7L1B {} +interface I7Flex extends I7L2A, I7L2B {} + +final class I7Impl implements I7Flex { + public function set(int|string $v): void { echo "I7: ", var_export($v, true), "\n"; } +} + +interface I8Setter<-X> { public function set(X $v): void; } +interface I8Flex extends I8Setter, I8Setter {} + +final class I8Impl implements I8Flex { + public function set(mixed $v): void { echo "I8: ", var_export($v, true), "\n"; } +} + +interface I9Getter<+X : object> { public function get(): X; } +interface I9Flex extends I9Getter, I9Getter {} + +final class I9Impl implements I9Flex { + public function get(): FooBar { return new FooBar(); } +} + +(new I1Impl)->set(7); (new I1Impl)->set("hi"); +(new I2Impl)->set(7); (new I2Impl)->set("hi"); +(new I3Impl)->set(7); (new I3Impl)->set("hi"); (new I3Impl)->set(3.14); +var_dump((new I4Impl)->get() instanceof IFoo, (new I4Impl)->get() instanceof IBar); +var_dump((new I5Impl)->get() instanceof IFoo, (new I5Impl)->get() instanceof IBar, (new I5Impl)->get() instanceof IBaz); +$i6 = new I6Impl; var_dump($i6->get() instanceof FooBar); $i6->set(1); $i6->set("x"); +(new I7Impl)->set(7); (new I7Impl)->set("hi"); +(new I8Impl)->set(7); (new I8Impl)->set("hi"); (new I8Impl)->set(3.14); +var_dump((new I9Impl)->get() instanceof FooBar); + +echo "done\n"; +?> +--EXPECT-- +I1: 7 +I1: 'hi' +I2: 7 +I2: 'hi' +I3: 7 +I3: 'hi' +I3: 3.14 +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +I6.set: 1 +I6.set: 'x' +I7: 7 +I7: 'hi' +I8: 7 +I8: 'hi' +I8: 3.14 +bool(true) +done diff --git a/Zend/tests/generics/inheritance/diamond/interface_diamond_variance_invariant_rejected.phpt b/Zend/tests/generics/inheritance/diamond/interface_diamond_variance_invariant_rejected.phpt new file mode 100644 index 000000000000..e4a76aed950f --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/interface_diamond_variance_invariant_rejected.phpt @@ -0,0 +1,10 @@ +--TEST-- +Diamond + variance: invariant T interface diamond merges via use-site variance +--FILE-- + {} +interface FlexBox extends Box, Box {} +echo (new ReflectionClass(FlexBox::class))->getMethods() === [] ? "ok\n" : "unexpected\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_bad_impl_rejected.phpt b/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_bad_impl_rejected.phpt new file mode 100644 index 000000000000..0377755a4911 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_bad_impl_rejected.phpt @@ -0,0 +1,20 @@ +--TEST-- +Diamond + invariant T: bad concrete implementer is rejected by per-path LSP +--FILE-- + { + public function tap(T $object): T; +} + +interface FooTapper extends Tapper {} +interface BarTapper extends Tapper {} + +class Bad implements FooTapper, BarTapper { + public function tap(IFoo $object): IFoo { return $object; } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::tap(IFoo $object): IFoo must be compatible with Tapper::tap(IBar $object): IBar in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_with_use_site_variance.phpt b/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_with_use_site_variance.phpt new file mode 100644 index 000000000000..65176762e366 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_with_use_site_variance.phpt @@ -0,0 +1,34 @@ +--TEST-- +Diamond + invariant T: a concrete class can satisfy multiple bindings via use-site variance (param union, return intersection) +--FILE-- + { + public function tap(T $object): T; +} + +interface FooTapper extends Tapper {} +interface BarTapper extends Tapper {} + +final class FooBarTapper implements FooTapper, BarTapper, Tapper { + public function tap(IFoo | IBar $object): IFoo & IBar { + if ($object instanceof IFoo && $object instanceof IBar) { + return $object; + } + return new FooBar(); + } +} + +$t = new FooBarTapper(); +var_dump($t->tap(new FooBar()) instanceof FooBar); +var_dump($t instanceof FooTapper, $t instanceof BarTapper, $t instanceof Tapper); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_bad_impl_rejected.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_bad_impl_rejected.phpt new file mode 100644 index 000000000000..c7d735bcdd2a --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_bad_impl_rejected.phpt @@ -0,0 +1,19 @@ +--TEST-- +Diamond + invariant T: class missing a path of the use-site-merged interface contract is rejected +--FILE-- + { + public function tap(T $object): T; +} + +interface FlexTapper extends Tapper, Tapper {} + +class Bad implements FlexTapper { + public function tap(IFoo $o): IFoo { return $o; } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::tap(IFoo $o): IFoo must be compatible with %s::tap(IFoo|IBar $object): IFoo&IBar in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_class_impl.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_class_impl.phpt new file mode 100644 index 000000000000..95fa0870079c --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_class_impl.phpt @@ -0,0 +1,31 @@ +--TEST-- +Diamond + invariant T: class implementing a use-site-merged interface diamond +--FILE-- + { + public function tap(T $object): T; +} + +interface FlexTapper extends Tapper, Tapper {} + +class Concrete implements FlexTapper { + public function tap(IFoo|IBar $o): IFoo&IBar { + if ($o instanceof IFoo && $o instanceof IBar) { + return $o; + } + return new FooBar(); + } +} + +$c = new Concrete(); +var_dump($c->tap(new FooBar()) instanceof FooBar); +var_dump($c instanceof FlexTapper, $c instanceof Tapper); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_merge.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_merge.phpt new file mode 100644 index 000000000000..ace90e13e289 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_merge.phpt @@ -0,0 +1,32 @@ +--TEST-- +Diamond + invariant T: interface diamond synthesises use-site-merged abstract contract +--FILE-- + { + public function tap(T $object): T; +} + +interface FooTapper extends Tapper {} +interface BarTapper extends Tapper {} + +interface FlexTapper extends FooTapper, BarTapper {} + +$r = new ReflectionClass(FlexTapper::class); +$m = $r->getMethod('tap'); +echo "param: ", (string)$m->getParameters()[0]->getType(), "\n"; +echo "return: ", (string)$m->getReturnType(), "\n"; + +interface FlexDirect extends Tapper, Tapper {} +$rd = new ReflectionClass(FlexDirect::class); +$md = $rd->getMethod('tap'); +echo "direct param: ", (string)$md->getParameters()[0]->getType(), "\n"; +echo "direct return: ", (string)$md->getReturnType(), "\n"; +?> +--EXPECT-- +param: IFoo|IBar +return: IFoo&IBar +direct param: IFoo|IBar +direct return: IFoo&IBar diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt new file mode 100644 index 000000000000..aa460a7a88c3 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt @@ -0,0 +1,11 @@ +--TEST-- +Diamond + invariant T: arity mismatch is the one remaining rejection at the diamond stage +--FILE-- + {} +interface Alpha extends Multi {} +interface Beta extends Multi {} +PHP +?> +--EXPECTF-- +Fatal error: %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/invariant_tapper_class_diamond.phpt b/Zend/tests/generics/inheritance/diamond/invariant_tapper_class_diamond.phpt new file mode 100644 index 000000000000..184f75bf62f5 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_tapper_class_diamond.phpt @@ -0,0 +1,52 @@ +--TEST-- +Diamond + invariant T: Tapper with T in both parameter and return positions, concrete class satisfies all paths via use-site variance +--FILE-- + { + public function tap(T $object): T; +} + +interface FooTapper extends Tapper {} +interface BarTapper extends Tapper {} + +class FooBarTapper implements FooTapper, BarTapper, Tapper { + public function tap(IFoo | IBar $object): IFoo & IBar { + if ($object instanceof IFoo) { + if ($object instanceof IBar) { + return $object; + } + + return new FooBar(); + } else { + return new FooBar(); + } + } +} + +$tapper = new FooBarTapper; +$a = $tapper->tap(new Foo()); +$b = $tapper->tap(new Bar()); +$c = $tapper->tap(new FooBar()); + +assert($a instanceof IFoo); +assert($a instanceof IBar); + +assert($b instanceof IFoo); +assert($b instanceof IBar); + +assert($c instanceof IFoo); +assert($c instanceof IBar); + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/inheritance/diamond/matching_args_via_two_paths.phpt b/Zend/tests/generics/inheritance/diamond/matching_args_via_two_paths.phpt new file mode 100644 index 000000000000..189699e23766 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/matching_args_via_two_paths.phpt @@ -0,0 +1,13 @@ +--TEST-- +Diamond: same args via two paths is accepted +--FILE-- + {} +interface ABox extends Box {} +interface BBox extends Box {} + +class C implements ABox, BBox {} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/diamond/nullable_intersection_merge.phpt b/Zend/tests/generics/inheritance/diamond/nullable_intersection_merge.phpt new file mode 100644 index 000000000000..ad7533afba24 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/nullable_intersection_merge.phpt @@ -0,0 +1,28 @@ +--TEST-- +Diamond + nullable: covariant intersection allows null only when every path does +--FILE-- + { public function get(): ?X; } + +interface Flex extends Getter, Getter {} +$r = new ReflectionClass(Flex::class); +echo "merged: ", (string)$r->getMethod('get')->getReturnType(), "\n"; + +class Good1 implements Flex { + public function get(): (IFoo&IBar)|null { return null; } +} + +class Good2 implements Flex { + public function get(): IFoo&IBar { return new FooBar(); } +} + +var_dump((new Good1())->get(), (new Good2())->get() instanceof FooBar); +?> +--EXPECT-- +merged: IFoo&IBar|null +NULL +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/nullable_intersection_missing_branch_rejected.phpt b/Zend/tests/generics/inheritance/diamond/nullable_intersection_missing_branch_rejected.phpt new file mode 100644 index 000000000000..b16a59c7ee28 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/nullable_intersection_missing_branch_rejected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Diamond + nullable intersection: implementer missing one branch is rejected +--FILE-- + { public function get(): ?X; } +interface Flex extends Getter, Getter {} + +class Bad implements Flex { + public function get(): ?IFoo { return null; } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::get(): ?IFoo must be compatible with Getter::get(): IFoo&IBar|null in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/nullable_union_merge.phpt b/Zend/tests/generics/inheritance/diamond/nullable_union_merge.phpt new file mode 100644 index 000000000000..157df44803ae --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/nullable_union_merge.phpt @@ -0,0 +1,18 @@ +--TEST-- +Diamond + nullable: contravariant union propagates null from any operand +--FILE-- + { public function set(X $v): void; } + +interface Flex extends Setter, Setter {} +$r = new ReflectionClass(Flex::class); +echo "merged: ", (string)$r->getMethod('set')->getParameters()[0]->getType(), "\n"; + +class Good implements Flex { + public function set(int|string|null $v): void {} +} +echo "good ok\n"; +?> +--EXPECT-- +merged: string|int|null +good ok diff --git a/Zend/tests/generics/inheritance/diamond/nullable_union_missing_null_rejected.phpt b/Zend/tests/generics/inheritance/diamond/nullable_union_missing_null_rejected.phpt new file mode 100644 index 000000000000..958b48a633c7 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/nullable_union_missing_null_rejected.phpt @@ -0,0 +1,13 @@ +--TEST-- +Diamond + nullable union: implementer missing null in param is rejected +--FILE-- + { public function set(X $v): void; } +interface Flex extends Setter, Setter {} + +class Bad implements Flex { + public function set(int|string $v): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::set(string|int $v): void must be compatible with Setter::set(string|int|null $v): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/parent_supplies_interface_matches.phpt b/Zend/tests/generics/inheritance/diamond/parent_supplies_interface_matches.phpt new file mode 100644 index 000000000000..94f575d47011 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/parent_supplies_interface_matches.phpt @@ -0,0 +1,12 @@ +--TEST-- +Diamond: parent's transitive binding matches a direct interface binding +--FILE-- + {} +class P implements Box {} + +class C extends P implements Box {} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond.phpt b/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond.phpt new file mode 100644 index 000000000000..ddef3637f995 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond.phpt @@ -0,0 +1,32 @@ +--TEST-- +Diamond + property hook (get-only): covariant T merges return type as intersection +--FILE-- + { + public T $x { get; } +} +interface IFoo {} +interface IBar {} +class FooBar implements IFoo, IBar {} + +interface FlexX extends HasX, HasX {} + +$r = new ReflectionClass(FlexX::class); +$prop = $r->getProperty('x'); +$hooks = $prop->getHooks(); +foreach ($hooks as $kind => $hook) { + if ($hook->hasReturnType()) { + echo "hook $kind return: ", (string)$hook->getReturnType(), "\n"; + } +} + +class C implements FlexX { + public IFoo&IBar $x { get => new FooBar(); } +} +$c = new C; +var_dump($c->x instanceof IFoo, $c->x instanceof IBar); +?> +--EXPECT-- +hook get return: IFoo&IBar +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond_bad.phpt b/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond_bad.phpt new file mode 100644 index 000000000000..81cb4736a929 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond_bad.phpt @@ -0,0 +1,18 @@ +--TEST-- +Diamond + property hook (get-only): impl missing one branch of the merged intersection is rejected +--FILE-- + { + public T $x { get; } +} +interface IFoo {} +interface IBar {} + +interface FlexX extends HasX, HasX {} + +class Bad implements FlexX { + public IFoo $x { get => new class implements IFoo {}; } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::$x::get(): IFoo must be compatible with HasX::$x::get(): IFoo&IBar in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/reflection_merged_signatures.phpt b/Zend/tests/generics/inheritance/diamond/reflection_merged_signatures.phpt new file mode 100644 index 000000000000..11b81a469813 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/reflection_merged_signatures.phpt @@ -0,0 +1,50 @@ +--TEST-- +Reflection: merged diamond signatures are visible through ReflectionMethod / ReflectionType +--FILE-- + { + public function tap(T $o): T; +} + +interface FlexTapper extends Tapper, Tapper {} + +class C implements FlexTapper { + public function tap(IFoo|IBar $o): IFoo&IBar { + if ($o instanceof IFoo && $o instanceof IBar) { + return $o; + } + return new FooBar(); + } +} + +$flex = (new ReflectionClass(FlexTapper::class))->getMethod('tap'); +echo "FlexTapper::tap return = ", (string)$flex->getReturnType(), "\n"; +echo "FlexTapper::tap param = ", (string)$flex->getParameters()[0]->getType(), "\n"; +echo "FlexTapper::tap return is intersection: ", + ($flex->getReturnType() instanceof ReflectionIntersectionType ? "yes" : "no"), "\n"; +echo "FlexTapper::tap param is union: ", + ($flex->getParameters()[0]->getType() instanceof ReflectionUnionType ? "yes" : "no"), "\n"; + +$c = (new ReflectionClass(C::class))->getMethod('tap'); +echo "C::tap return = ", (string)$c->getReturnType(), "\n"; +echo "C::tap param = ", (string)$c->getParameters()[0]->getType(), "\n"; +echo "C::tap prototype declaring class = ", $c->getPrototype()->getDeclaringClass()->getName(), "\n"; + +$base = (new ReflectionClass(Tapper::class))->getMethod('tap'); +echo "Tapper::tap return = ", (string)$base->getReturnType(), "\n"; +echo "Tapper::tap param = ", (string)$base->getParameters()[0]->getType(), "\n"; +?> +--EXPECT-- +FlexTapper::tap return = IFoo&IBar +FlexTapper::tap param = IFoo|IBar +FlexTapper::tap return is intersection: yes +FlexTapper::tap param is union: yes +C::tap return = IFoo&IBar +C::tap param = IFoo|IBar +C::tap prototype declaring class = Tapper +Tapper::tap return = object +Tapper::tap param = object diff --git a/Zend/tests/generics/inheritance/diamond/scalar_intersection_class_diamond_rejected.phpt b/Zend/tests/generics/inheritance/diamond/scalar_intersection_class_diamond_rejected.phpt new file mode 100644 index 000000000000..35a6ea166b58 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_intersection_class_diamond_rejected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Diamond + invariant T: a class double-implementing with scalar bindings is caught by per-path LSP +--FILE-- + { + public function get(): T; +} +interface IntBox extends Box {} +interface StrBox extends Box {} + +class C implements IntBox, StrBox { + public function get() { return 0; } +} +?> +--EXPECTF-- +Fatal error: Declaration of C::get() must be compatible with Box::get(): int in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/scalar_intersection_covariant_rejected.phpt b/Zend/tests/generics/inheritance/diamond/scalar_intersection_covariant_rejected.phpt new file mode 100644 index 000000000000..b166332cdd6f --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_intersection_covariant_rejected.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond + covariant +T: scalar bindings in a covariant return position are rejected +--FILE-- + { + public function make(): T; +} +interface IntProducer extends Producer {} +interface StrProducer extends Producer {} + +interface FlexProducer extends IntProducer, StrProducer {} +?> +--EXPECTF-- +Fatal error: Diamond inheritance of Producer::make() in FlexProducer would require return type int&string, which is uninhabited; constrain the type parameter with an object bound in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/scalar_intersection_invariant_use_site_rejected.phpt b/Zend/tests/generics/inheritance/diamond/scalar_intersection_invariant_use_site_rejected.phpt new file mode 100644 index 000000000000..ae752a8bd4b6 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_intersection_invariant_use_site_rejected.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond + invariant T: scalar bindings at a use-site covariant return are rejected +--FILE-- + { + public function get(): T; +} +interface IntBox extends Box {} +interface StrBox extends Box {} + +interface FlexBox extends IntBox, StrBox {} +?> +--EXPECTF-- +Fatal error: Diamond inheritance of Box::get() in FlexBox would require return type int&string, which is uninhabited; constrain the type parameter with an object bound in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/scalar_intersection_trait_rejected.phpt b/Zend/tests/generics/inheritance/diamond/scalar_intersection_trait_rejected.phpt new file mode 100644 index 000000000000..0fc3de93172d --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_intersection_trait_rejected.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond + trait-use: scalar bindings at a covariant return position are rejected +--FILE-- + { + public function make(): T { throw new Error('stub'); } +} + +class C { + use Producer, Producer; +} +?> +--EXPECTF-- +Fatal error: Diamond inheritance of Producer::make() in C would require return type int&string, which is uninhabited; constrain the type parameter with an object bound in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/scalar_union_param_accepted.phpt b/Zend/tests/generics/inheritance/diamond/scalar_union_param_accepted.phpt new file mode 100644 index 000000000000..91e8210c4e68 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_union_param_accepted.phpt @@ -0,0 +1,17 @@ +--TEST-- +Diamond + contravariant -T: scalar bindings at a parameter position merge as a union (no rejection) +--FILE-- + { + public function sink(T $v): void; +} +interface IntSink extends Sink {} +interface StrSink extends Sink {} + +interface FlexSink extends IntSink, StrSink {} + +$r = new ReflectionClass(FlexSink::class); +echo (string)$r->getMethod('sink')->getParameters()[0]->getType(), "\n"; +?> +--EXPECT-- +string|int diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_compiles.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_compiles.phpt new file mode 100644 index 000000000000..cef8ac7736fd --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_compiles.phpt @@ -0,0 +1,26 @@ +--TEST-- +Diamond + trait-use: multiple uses of the same generic trait with different bindings merge use-site-variantly at the reflection level +--FILE-- + { + public function set(X $v): void { echo var_export($v, true), "\n"; } +} + +class C { + use Setter, Setter; +} + +$r = new ReflectionClass(C::class); +$type = $r->getMethod('set')->getParameters()[0]->getType(); +echo "param: ", (string)$type, "\n"; +echo "is union: ", ($type instanceof ReflectionUnionType ? "yes" : "no"), "\n"; + +$c = new C(); +$c->set(1); +$c->set("hello"); +?> +--EXPECT-- +param: string|int +is union: yes +1 +'hello' diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_covariant.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_covariant.phpt new file mode 100644 index 000000000000..f0f7645e0d28 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_covariant.phpt @@ -0,0 +1,29 @@ +--TEST-- +Diamond + trait-use: covariant return merges as intersection +--FILE-- + { + public function get(): X { return new FooBar(); } +} + +class C { + use HasGetter, HasGetter; +} + +$r = new ReflectionClass(C::class); +$type = $r->getMethod('get')->getReturnType(); +echo "return: ", (string)$type, "\n"; +echo "is intersection: ", ($type instanceof ReflectionIntersectionType ? "yes" : "no"), "\n"; + +$c = new C(); +var_dump($c->get() instanceof IFoo, $c->get() instanceof IBar); +?> +--EXPECT-- +return: IFoo&IBar +is intersection: yes +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_invariant_use_site.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_invariant_use_site.phpt new file mode 100644 index 000000000000..dfc0f7b12403 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_invariant_use_site.phpt @@ -0,0 +1,32 @@ +--TEST-- +Diamond + trait-use: invariant T (no marker) with T in both positions merges use-site +--FILE-- + { + public function pipe(T $x): T { return $x; } +} + +interface IFoo {} +interface IBar {} +class FooBar implements IFoo, IBar {} + +class C { + use Pipe, Pipe; +} + +$r = new ReflectionClass(C::class); +$m = $r->getMethod('pipe'); +echo "param: ", (string)$m->getParameters()[0]->getType(), "\n"; +echo "return: ", (string)$m->getReturnType(), "\n"; +echo "param is union: ", ($m->getParameters()[0]->getType() instanceof ReflectionUnionType ? "yes" : "no"), "\n"; +echo "return is intersection: ", ($m->getReturnType() instanceof ReflectionIntersectionType ? "yes" : "no"), "\n"; + +$c = new C(); +var_dump($c->pipe(new FooBar()) instanceof FooBar); +?> +--EXPECT-- +param: IFoo|IBar +return: IFoo&IBar +param is union: yes +return is intersection: yes +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_mixed_methods.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_mixed_methods.phpt new file mode 100644 index 000000000000..57e6dc64b11f --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_mixed_methods.phpt @@ -0,0 +1,28 @@ +--TEST-- +Diamond + trait-use: only T-touching methods are merged; non-generic methods are unaffected +--FILE-- + { + public function set(X $v): void { echo "set ", var_export($v, true), "\n"; } + public function ping(): string { return "pong"; } +} + +class C { + use Box, Box; +} + +$r = new ReflectionClass(C::class); +echo "set param: ", (string)$r->getMethod('set')->getParameters()[0]->getType(), "\n"; +echo "ping return: ", (string)$r->getMethod('ping')->getReturnType(), "\n"; + +$c = new C; +$c->set(1); +$c->set("hi"); +echo $c->ping(), "\n"; +?> +--EXPECT-- +set param: string|int +ping return: string +set 1 +set 'hi' +pong diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_multi_param_mixed.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_multi_param_mixed.phpt new file mode 100644 index 000000000000..319c10de75ba --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_multi_param_mixed.phpt @@ -0,0 +1,35 @@ +--TEST-- +Diamond + trait-use: multi-param mixed +T / -U, declared variance drives merge polarity +--FILE-- + { + public function get(): T { throw new Error('stub'); } + public function set(U $v): void { echo var_export($v, true), "\n"; } +} + +class C { + use IO, IO; + + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "set: ", var_export($v, true), "\n"; } +} + +$r = new ReflectionClass(C::class); +echo "get return: ", (string)$r->getMethod('get')->getReturnType(), "\n"; +echo "set param: ", (string)$r->getMethod('set')->getParameters()[0]->getType(), "\n"; + +$c = new C(); +var_dump($c->get() instanceof FooBar); +$c->set(7); +$c->set("hi"); +?> +--EXPECT-- +get return: IFoo&IBar +set param: string|int +bool(true) +set: 7 +set: 'hi' diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_three_uses.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_three_uses.phpt new file mode 100644 index 000000000000..cd7ef2f08aab --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_three_uses.phpt @@ -0,0 +1,26 @@ +--TEST-- +Diamond + trait-use: three uses of the same generic trait fold pairwise into one merged contract +--FILE-- + { + public function sink(X $v): void { echo var_export($v, true), "\n"; } +} + +class C { + use Sink, Sink, Sink; +} + +$r = new ReflectionClass(C::class); +$type = $r->getMethod('sink')->getParameters()[0]->getType(); +echo "param: ", (string)$type, "\n"; + +$c = new C; +$c->sink(1); +$c->sink("x"); +$c->sink(3.14); +?> +--EXPECT-- +param: string|int|float +1 +'x' +3.14 diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_variadic.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_variadic.phpt new file mode 100644 index 000000000000..df7fde63aa83 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_variadic.phpt @@ -0,0 +1,28 @@ +--TEST-- +Diamond + trait-use: variadic contravariant param merges as union +--FILE-- + { + public function sink(X ...$xs): void { + foreach ($xs as $x) echo var_export($x, true), "\n"; + } +} + +class C { + use Sink, Sink; +} + +$r = new ReflectionClass(C::class); +$type = $r->getMethod('sink')->getParameters()[0]->getType(); +echo "param: ", (string)$type, "\n"; +echo "is union: ", ($type instanceof ReflectionUnionType ? "yes" : "no"), "\n"; + +(new C)->sink(1, "x", 2, "y"); +?> +--EXPECT-- +param: string|int +is union: yes +1 +'x' +2 +'y' diff --git a/Zend/tests/generics/inheritance/diamond/variadic_bad_impl_rejected.phpt b/Zend/tests/generics/inheritance/diamond/variadic_bad_impl_rejected.phpt new file mode 100644 index 000000000000..64bdeaab7a66 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/variadic_bad_impl_rejected.phpt @@ -0,0 +1,11 @@ +--TEST-- +Diamond + variadic: bad impl rejected by LSP with correctly substituted error message +--FILE-- + { public function sink(X ...$xs): void; } +class Bad implements Sink, Sink { + public function sink(int ...$xs): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::sink(int ...$xs): void must be compatible with Sink::sink(string ...$xs): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/variadic_contravariant_diamond.phpt b/Zend/tests/generics/inheritance/diamond/variadic_contravariant_diamond.phpt new file mode 100644 index 000000000000..882b6c2c2093 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/variadic_contravariant_diamond.phpt @@ -0,0 +1,39 @@ +--TEST-- +Diamond + variadic: contravariant T variadic merges as union, exercised at runtime +--FILE-- + { public function sink(X ...$xs): void; } + +class A implements Sink, Sink { + public function sink(int|string ...$xs): void { + foreach ($xs as $x) echo "A: ", var_export($x, true), "\n"; + } +} + +interface IntSink extends Sink {} +interface StrSink extends Sink {} +class B implements IntSink, StrSink { + public function sink(int|string ...$xs): void { + foreach ($xs as $x) echo "B: ", var_export($x, true), "\n"; + } +} + +interface FlexSink extends IntSink, StrSink {} +class C implements FlexSink { + public function sink(int|string ...$xs): void { + foreach ($xs as $x) echo "C: ", var_export($x, true), "\n"; + } +} + +(new A)->sink(1, "x", 2); +(new B)->sink("y", 3); +(new C)->sink(4, "z"); +?> +--EXPECT-- +A: 1 +A: 'x' +A: 2 +B: 'y' +B: 3 +C: 4 +C: 'z' diff --git a/Zend/tests/generics/inheritance/diamond/variance_positive_all_paths.phpt b/Zend/tests/generics/inheritance/diamond/variance_positive_all_paths.phpt new file mode 100644 index 000000000000..3c71f2e55417 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/variance_positive_all_paths.phpt @@ -0,0 +1,234 @@ +--TEST-- +Diamond + variance: all positive paths: contravariant union, covariant intersection +--FILE-- + { + public function set(X $v): void; +} + +final class A1Impl implements A1Setter, A1Setter { + public function set(int|string $v): void { echo "A1: ", var_export($v, true), "\n"; } +} + +interface A2Setter<-X> { + public function set(X $v): void; +} + +final class A2Impl implements A2Setter, A2Setter, A2Setter { + public function set(int|string|float $v): void { echo "A2: ", var_export($v, true), "\n"; } +} + +interface A3Setter<-X> { + public function set(X $v): void; +} + +interface A3IntSink extends A3Setter {} +interface A3StrSink extends A3Setter {} +final class A3Impl implements A3IntSink, A3StrSink { + public function set(int|string $v): void { echo "A3: ", var_export($v, true), "\n"; } +} + +interface A5Setter<-X> { + public function set(X $v): void; +} +interface A5IntL1 extends A5Setter {} +interface A5IntL2 extends A5IntL1 {} +interface A5StrL1 extends A5Setter {} +interface A5StrL2 extends A5StrL1 {} +final class A5Impl implements A5IntL2, A5StrL2 { + public function set(int|string $v): void { echo "A5: ", var_export($v, true), "\n"; } +} + +class A6Apple {} +class A6Orange {} +interface A6Setter<-X> { + public function set(X $v): void; +} +final class A6Impl implements A6Setter, A6Setter { + public function set(A6Apple|A6Orange $v): void { echo "A6: ", $v::class, "\n"; } +} + +interface A7Sink<-X> { + public function setOne(X $v): void; + public function setTwo(X $other): void; +} +final class A7Impl implements A7Sink, A7Sink { + public function setOne(int|string $v): void { echo "A7.setOne: ", var_export($v, true), "\n"; } + public function setTwo(int|string $other): void { echo "A7.setTwo: ", var_export($other, true), "\n"; } +} + +interface A8Setter<-X> { + public function set(X $v): void; +} +final class A8Impl implements A8Setter, A8Setter { + public function set(mixed $v): void { echo "A8: ", var_export($v, true), "\n"; } +} + + +interface B1Getter<+X : object> { + public function get(): X; +} +final class B1Impl implements B1Getter, B1Getter { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface B2Getter<+X : object> { + public function get(): X; +} +final class B2Impl implements B2Getter, B2Getter, B2Getter { + public function get(): IFoo&IBar&IBaz { return new FooBarBaz(); } +} + +interface B3Getter<+X : object> { + public function get(): X; +} +interface B3FooSrc extends B3Getter {} +interface B3BarSrc extends B3Getter {} +final class B3Impl implements B3FooSrc, B3BarSrc { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface B4Getter<+X : object> { + public function get(): X; +} +abstract class B4ParentSrc implements B4Getter { + public abstract function get(): IFoo; +} +final class B4Impl extends B4ParentSrc implements B4Getter { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface B5Getter<+X : object> { + public function get(): X; +} +interface B5FooL1 extends B5Getter {} +interface B5FooL2 extends B5FooL1 {} +interface B5BarL1 extends B5Getter {} +interface B5BarL2 extends B5BarL1 {} +final class B5Impl implements B5FooL2, B5BarL2 { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface B6Getter<+X : object> { + public function get(): X; +} +final class B6Impl implements B6Getter, B6Getter { + public function get(): FooBar { return new FooBar(); } +} + + +interface C1IO<+X : object, -Y> { + public function get(): X; + public function set(Y $v): void; +} +final class C1Impl implements C1IO, C1IO { + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "C1.set: ", var_export($v, true), "\n"; } +} + +interface C2IO<+X : object, -Y> { + public function get(): X; + public function set(Y $v): void; +} +interface C2FooInt extends C2IO {} +interface C2BarStr extends C2IO {} +final class C2Impl implements C2FooInt, C2BarStr { + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "C2.set: ", var_export($v, true), "\n"; } +} + +interface C3IO<+X : object, -Y> { + public function get(): X; + public function set(Y $v): void; +} +interface C3FooIntL1 extends C3IO {} +interface C3FooIntL2 extends C3FooIntL1 {} +interface C3BarStrL1 extends C3IO {} +interface C3BarStrL2 extends C3BarStrL1 {} +final class C3Impl implements C3FooIntL2, C3BarStrL2 { + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "C3.set: ", var_export($v, true), "\n"; } +} + + +interface D1Setter<-X> { + public function set(X $v): void; +} +interface D1Wrap<-Y> extends D1Setter {} +final class D1Impl implements D1Wrap, D1Setter { + public function set(int|string $v): void { echo "D1: ", var_export($v, true), "\n"; } +} + + +$a1 = new A1Impl(); $a1->set(7); $a1->set("hi"); +$a2 = new A2Impl(); $a2->set(7); $a2->set("hi"); $a2->set(3.14); +$a3 = new A3Impl(); $a3->set(7); $a3->set("hi"); +$a5 = new A5Impl(); $a5->set(7); $a5->set("hi"); +$a6 = new A6Impl(); $a6->set(new A6Apple()); $a6->set(new A6Orange()); +$a7 = new A7Impl(); $a7->setOne(7); $a7->setTwo("hi"); +$a8 = new A8Impl(); $a8->set(7); $a8->set("hi"); $a8->set(3.14); + +$b1 = new B1Impl(); var_dump($b1->get() instanceof IFoo, $b1->get() instanceof IBar); +$b2 = new B2Impl(); var_dump($b2->get() instanceof IFoo, $b2->get() instanceof IBar, $b2->get() instanceof IBaz); +$b3 = new B3Impl(); var_dump($b3->get() instanceof IFoo, $b3->get() instanceof IBar); +$b4 = new B4Impl(); var_dump($b4->get() instanceof IFoo, $b4->get() instanceof IBar); +$b5 = new B5Impl(); var_dump($b5->get() instanceof IFoo, $b5->get() instanceof IBar); +$b6 = new B6Impl(); var_dump($b6->get() instanceof FooBar); + +$c1 = new C1Impl(); var_dump($c1->get() instanceof FooBar); $c1->set(7); $c1->set("hi"); +$c2 = new C2Impl(); var_dump($c2->get() instanceof FooBar); $c2->set(7); $c2->set("hi"); +$c3 = new C3Impl(); var_dump($c3->get() instanceof FooBar); $c3->set(7); $c3->set("hi"); + +$d1 = new D1Impl(); $d1->set(7); $d1->set("hi"); + +echo "done\n"; +?> +--EXPECT-- +A1: 7 +A1: 'hi' +A2: 7 +A2: 'hi' +A2: 3.14 +A3: 7 +A3: 'hi' +A5: 7 +A5: 'hi' +A6: A6Apple +A6: A6Orange +A7.setOne: 7 +A7.setTwo: 'hi' +A8: 7 +A8: 'hi' +A8: 3.14 +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +C1.set: 7 +C1.set: 'hi' +bool(true) +C2.set: 7 +C2.set: 'hi' +bool(true) +C3.set: 7 +C3.set: 'hi' +D1: 7 +D1: 'hi' +done diff --git a/Zend/tests/generics/inheritance/lsp/abstract_class_chain.phpt b/Zend/tests/generics/inheritance/lsp/abstract_class_chain.phpt new file mode 100644 index 000000000000..674b98e12375 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/abstract_class_chain.phpt @@ -0,0 +1,20 @@ +--TEST-- +Parametric LSP: abstract generic class extended by another abstract class, then concretized +--FILE-- + { + abstract public function process(T $x): T; +} + +abstract class Middle extends Base { + public function double(U $x): U { return $this->process($this->process($x)); } +} + +class IntLeaf extends Middle { + public function process(int $x): int { return $x + 1; } +} + +echo (new IntLeaf)->double(10), "\n"; +?> +--EXPECT-- +12 diff --git a/Zend/tests/generics/inheritance/lsp/abstract_method.phpt b/Zend/tests/generics/inheritance/lsp/abstract_method.phpt new file mode 100644 index 000000000000..27773e88f331 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/abstract_method.phpt @@ -0,0 +1,17 @@ +--TEST-- +Parametric LSP: substitution applies to abstract methods inherited from a generic ancestor +--FILE-- + { + abstract public function unwrap(): T; +} + +class IntContainer extends Container { + public function __construct(private int $v) {} + public function unwrap(): int { return $this->v; } +} + +echo (new IntContainer(11))->unwrap(), "\n"; +?> +--EXPECT-- +11 diff --git a/Zend/tests/generics/inheritance/lsp/basic_param_and_return.phpt b/Zend/tests/generics/inheritance/lsp/basic_param_and_return.phpt new file mode 100644 index 000000000000..2b96fb813731 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/basic_param_and_return.phpt @@ -0,0 +1,29 @@ +--TEST-- +Parametric LSP: child overrides param and return with the bound argument +--FILE-- + { + public function put(T $value): void; + public function get(): T; +} + +class IntBag implements Bag { + private int $v = 0; + public function put(int $value): void { $this->v = $value; } + public function get(): int { return $this->v; } +} + +$b = new IntBag; +$b->put(42); +echo $b->get(), "\n"; + +try { + /** @phpstan-ignore-next-line */ + $b->put("no"); +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +42 +TypeError diff --git a/Zend/tests/generics/inheritance/lsp/bounded_param.phpt b/Zend/tests/generics/inheritance/lsp/bounded_param.phpt new file mode 100644 index 000000000000..858387ac74b7 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/bounded_param.phpt @@ -0,0 +1,19 @@ +--TEST-- +Parametric LSP: substitution still composes with a bounded type parameter +--FILE-- + { + public function add(T $x): void; +} + +class DogBag implements Bag { + public function add(Dog $x): void { echo "added ", $x->name(), "\n"; } +} + +(new DogBag)->add(new Dog); +?> +--EXPECT-- +added dog diff --git a/Zend/tests/generics/inheritance/lsp/by_reference_substituted.phpt b/Zend/tests/generics/inheritance/lsp/by_reference_substituted.phpt new file mode 100644 index 000000000000..adbf9729176b --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/by_reference_substituted.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: by-reference parameter substituted with the bound argument +--FILE-- + { + public function fill(T &$slot): void; +} + +class IntFiller implements Filler { + public function fill(int &$slot): void { $slot = 99; } +} + +$x = 0; +(new IntFiller)->fill($x); +echo $x, "\n"; +?> +--EXPECT-- +99 diff --git a/Zend/tests/generics/inheritance/lsp/contravariant_param.phpt b/Zend/tests/generics/inheritance/lsp/contravariant_param.phpt new file mode 100644 index 000000000000..e7420179aeb9 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/contravariant_param.phpt @@ -0,0 +1,21 @@ +--TEST-- +Parametric LSP: contravariant parameter — child accepts a supertype of the bound argument +--FILE-- + { + public function eat(T $x): void; +} + +class DogEater implements Eater { + public function eat(Animal $x): void { echo "ate ", get_class($x), "\n"; } +} + +(new DogEater)->eat(new Dog); +(new DogEater)->eat(new Animal); +?> +--EXPECT-- +ate Dog +ate Animal diff --git a/Zend/tests/generics/inheritance/lsp/covariant_return.phpt b/Zend/tests/generics/inheritance/lsp/covariant_return.phpt new file mode 100644 index 000000000000..0985b93de5a2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/covariant_return.phpt @@ -0,0 +1,19 @@ +--TEST-- +Parametric LSP: covariant return — child returns a strict subtype of the bound argument +--FILE-- + { + public function produce(): T; +} + +class DogProducer implements Producer { + public function produce(): Dog { return new Dog; } +} + +echo get_class((new DogProducer)->produce()), "\n"; +?> +--EXPECT-- +Dog diff --git a/Zend/tests/generics/inheritance/lsp/default_param.phpt b/Zend/tests/generics/inheritance/lsp/default_param.phpt new file mode 100644 index 000000000000..648b477c78b2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/default_param.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: a default type parameter still allows the child's signature to be erased-compatible +--FILE-- + { + public function set(T $value): void; +} + +class IntHolder implements Holder { + public function set(int $value): void { echo "set $value\n"; } +} + +(new IntHolder)->set(5); +?> +--EXPECT-- +set 5 diff --git a/Zend/tests/generics/inheritance/lsp/diamond_via_interfaces.phpt b/Zend/tests/generics/inheritance/lsp/diamond_via_interfaces.phpt new file mode 100644 index 000000000000..2ac2872cafc7 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/diamond_via_interfaces.phpt @@ -0,0 +1,19 @@ +--TEST-- +Parametric LSP: diamond inheritance via two generic intermediaries with the same target +--FILE-- + { + public function id(T $x): T; +} + +interface LeftPath extends Base {} +interface RightPath extends Base {} + +class IntDiamond implements LeftPath, RightPath { + public function id(int $x): int { return $x; } +} + +echo (new IntDiamond)->id(99), "\n"; +?> +--EXPECT-- +99 diff --git a/Zend/tests/generics/inheritance/lsp/extends_and_implements_generic.phpt b/Zend/tests/generics/inheritance/lsp/extends_and_implements_generic.phpt new file mode 100644 index 000000000000..d2a7f87fccd2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/extends_and_implements_generic.phpt @@ -0,0 +1,23 @@ +--TEST-- +Parametric LSP: a class extending a generic class while implementing an unrelated generic interface +--FILE-- + { + public function append(T $value): void { echo "append:$value\n"; } +} + +interface Counter { + public function count(): U; +} + +class IntListWithCount extends BaseList implements Counter { + public function count(): int { return 0; } +} + +$l = new IntListWithCount; +$l->append(7); +echo $l->count(), "\n"; +?> +--EXPECT-- +append:7 +0 diff --git a/Zend/tests/generics/inheritance/lsp/generic_class_extends_generic.phpt b/Zend/tests/generics/inheritance/lsp/generic_class_extends_generic.phpt new file mode 100644 index 000000000000..0705d638d7c2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/generic_class_extends_generic.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: a generic class extending a generic class forwards bindings to subclasses +--FILE-- + { + public function f(X $x): void {} +} + +class B extends A {} + +class C extends B { + public function f(int $x): void { echo "C::f($x)\n"; } +} + +(new C)->f(7); +?> +--EXPECT-- +C::f(7) diff --git a/Zend/tests/generics/inheritance/lsp/method_param_separate_scope.phpt b/Zend/tests/generics/inheritance/lsp/method_param_separate_scope.phpt new file mode 100644 index 000000000000..ef2fbdc435b8 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/method_param_separate_scope.phpt @@ -0,0 +1,20 @@ +--TEST-- +Parametric LSP: a method-level generic parameter is not affected by class-level substitution +--FILE-- + { + public function get(): T; + public function map(callable $f): U; +} + +class IntContainer implements Container { + public function get(): int { return 5; } + public function map(callable $f): U { return $f(42); } +} + +echo (new IntContainer)->get(), "\n"; +echo (new IntContainer)->map(fn($x) => "v=$x"), "\n"; +?> +--EXPECT-- +5 +v=42 diff --git a/Zend/tests/generics/inheritance/lsp/method_substituted.phpt b/Zend/tests/generics/inheritance/lsp/method_substituted.phpt new file mode 100644 index 000000000000..ec3fd14778b5 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/method_substituted.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: methods on the parent are substituted in the child +--FILE-- + { + abstract public function make(): T; +} + +class IntFactory extends Factory { + public function make(): int { return 7; } +} + +echo (new IntFactory())->make(), "\n"; +?> +--EXPECT-- +7 diff --git a/Zend/tests/generics/inheritance/lsp/multi_interface.phpt b/Zend/tests/generics/inheritance/lsp/multi_interface.phpt new file mode 100644 index 000000000000..12f6fbff896e --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/multi_interface.phpt @@ -0,0 +1,25 @@ +--TEST-- +Parametric LSP: implementing two distinct generic interfaces with different bindings +--FILE-- + { + public function read(): T; +} + +interface Writer { + public function write(U $value): void; +} + +class IntReaderStringWriter implements Reader, Writer { + private string $w = ""; + public function read(): int { return 42; } + public function write(string $value): void { $this->w = $value; echo "wrote: $value\n"; } +} + +$x = new IntReaderStringWriter; +echo $x->read(), "\n"; +$x->write("hello"); +?> +--EXPECT-- +42 +wrote: hello diff --git a/Zend/tests/generics/inheritance/lsp/multi_param.phpt b/Zend/tests/generics/inheritance/lsp/multi_param.phpt new file mode 100644 index 000000000000..36775f44b5e4 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/multi_param.phpt @@ -0,0 +1,23 @@ +--TEST-- +Parametric LSP: multi-parameter generic interface substitutes both parameters +--FILE-- + { + public function set(K $key, V $value): void; + public function lookup(K $key): V; +} + +class StrIntMap implements Map { + private array $items = []; + public function set(string $key, int $value): void { $this->items[$key] = $value; } + public function lookup(string $key): int { return $this->items[$key] ?? -1; } +} + +$m = new StrIntMap; +$m->set("a", 1); +echo $m->lookup("a"), "\n"; +echo $m->lookup("missing"), "\n"; +?> +--EXPECT-- +1 +-1 diff --git a/Zend/tests/generics/inheritance/lsp/non_generic_inherits_unrelated.phpt b/Zend/tests/generics/inheritance/lsp/non_generic_inherits_unrelated.phpt new file mode 100644 index 000000000000..19dc88618b12 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/non_generic_inherits_unrelated.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: a non-generic class still uses standard LSP when the parent is non-generic +--FILE-- +f(3), "\n"; +?> +--EXPECT-- +8 diff --git a/Zend/tests/generics/inheritance/lsp/nullable_T_chain_subst.phpt b/Zend/tests/generics/inheritance/lsp/nullable_T_chain_subst.phpt new file mode 100644 index 000000000000..dad6ab6004df --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/nullable_T_chain_subst.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: ?T return substitutes through a multi-level chain (interface extending interface) +--FILE-- + { + public function next(): ?T; +} + +interface J extends I {} + +class C implements J { + public function next(): ?int { return 7; } +} + +var_dump((new C)->next()); +?> +--EXPECT-- +int(7) diff --git a/Zend/tests/generics/inheritance/lsp/nullable_T_param_subst.phpt b/Zend/tests/generics/inheritance/lsp/nullable_T_param_subst.phpt new file mode 100644 index 000000000000..37c97e94b636 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/nullable_T_param_subst.phpt @@ -0,0 +1,14 @@ +--TEST-- +Parametric LSP: child param narrower than ?T (substituted to ?int) is rejected +--FILE-- + { + public function take(?T $x): void {} +} + +class B extends A { + public function take(int $x): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of B::take(int $x): void must be compatible with A::take(?int $x): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/nullable_T_return_subst.phpt b/Zend/tests/generics/inheritance/lsp/nullable_T_return_subst.phpt new file mode 100644 index 000000000000..0760cc0b9a5a --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/nullable_T_return_subst.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: ?T return type substitutes through implements with the nullable bit preserved +--FILE-- + { + public function f(): ?T; +} + +class B implements A { + public function f(): ?int { return 1; } +} + +var_dump((new B)->f()); +?> +--EXPECT-- +int(1) diff --git a/Zend/tests/generics/inheritance/lsp/optional_arg_substituted.phpt b/Zend/tests/generics/inheritance/lsp/optional_arg_substituted.phpt new file mode 100644 index 000000000000..4b89ae236d43 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/optional_arg_substituted.phpt @@ -0,0 +1,27 @@ +--TEST-- +Parametric LSP: optional parameter substituted with the bound argument +--FILE-- + { + public function set(T $value, bool $flag = false): void; +} + +class IntMaybe implements Maybe { + public function set(int $value, bool $flag = false): void { echo "$value/", (int)$flag, "\n"; } +} + +$m = new IntMaybe; +$m->set(5); +$m->set(6, true); + +try { + /** @phpstan-ignore-next-line */ + $m->set("x"); +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +5/0 +6/1 +TypeError diff --git a/Zend/tests/generics/inheritance/lsp/parent_call.phpt b/Zend/tests/generics/inheritance/lsp/parent_call.phpt new file mode 100644 index 000000000000..89dee35e724b --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/parent_call.phpt @@ -0,0 +1,22 @@ +--TEST-- +Parametric LSP: calling parent::method() from substituted child still routes to the erased parent +--FILE-- + { + public function f(T $x): void { + echo "C::f got ", get_debug_type($x), "\n"; + } +} + +class IntC extends C { + public function f(int $x): void { + echo "IntC::f got $x\n"; + parent::f($x); + } +} + +(new IntC)->f(3); +?> +--EXPECT-- +IntC::f got 3 +C::f got int diff --git a/Zend/tests/generics/inheritance/lsp/reordered_params.phpt b/Zend/tests/generics/inheritance/lsp/reordered_params.phpt new file mode 100644 index 000000000000..a47500267117 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/reordered_params.phpt @@ -0,0 +1,32 @@ +--TEST-- +Parametric LSP: child reorders the parent's type parameters +--FILE-- + { + public function first(): A; + public function second(): B; +} + +class IntStrPair implements Pair { + public function __construct(private int $a, private string $b) {} + public function first(): int { return $this->a; } + public function second(): string { return $this->b; } +} + +class FlippedPair implements Pair { + public function __construct(private mixed $x, private mixed $y) {} + public function first(): Y { return $this->y; } + public function second(): X { return $this->x; } +} + +class StrIntFlipped extends FlippedPair {} + +$p = new IntStrPair(1, "hi"); +echo $p->first(), "/", $p->second(), "\n"; + +$f = new StrIntFlipped(7, "world"); +echo $f->first(), "/", $f->second(), "\n"; +?> +--EXPECT-- +1/hi +world/7 diff --git a/Zend/tests/generics/inheritance/lsp/transitive_three_level.phpt b/Zend/tests/generics/inheritance/lsp/transitive_three_level.phpt new file mode 100644 index 000000000000..99d541d5014d --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/transitive_three_level.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: substitution composes across three levels of generic ancestors +--FILE-- + { + public function take(T $x): T; +} +interface M extends L {} +interface N extends M {} + +class IntN implements N { + public function take(int $x): int { return $x + 1; } +} + +echo (new IntN)->take(41), "\n"; +?> +--EXPECT-- +42 diff --git a/Zend/tests/generics/inheritance/lsp/transitive_through_class.phpt b/Zend/tests/generics/inheritance/lsp/transitive_through_class.phpt new file mode 100644 index 000000000000..9f02cefc3f9e --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/transitive_through_class.phpt @@ -0,0 +1,19 @@ +--TEST-- +Parametric LSP: substitution composes through a generic abstract class and a concrete leaf +--FILE-- + { + public function area(): T; +} + +abstract class Polygon implements Shape {} + +class IntSquare extends Polygon { + public function __construct(private int $side) {} + public function area(): int { return $this->side * $this->side; } +} + +echo (new IntSquare(4))->area(), "\n"; +?> +--EXPECT-- +16 diff --git a/Zend/tests/generics/inheritance/lsp/transitive_two_level.phpt b/Zend/tests/generics/inheritance/lsp/transitive_two_level.phpt new file mode 100644 index 000000000000..0a78e360a6e2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/transitive_two_level.phpt @@ -0,0 +1,20 @@ +--TEST-- +Parametric LSP: substitution composes through a generic intermediate +--FILE-- + { + public function consume(T $value): void; +} + +interface Service extends Consumer {} + +class NullService implements Service { + public function consume(null $value): void { + echo "consumed null\n"; + } +} + +(new NullService)->consume(null); +?> +--EXPECT-- +consumed null diff --git a/Zend/tests/generics/inheritance/lsp/variadic_substituted.phpt b/Zend/tests/generics/inheritance/lsp/variadic_substituted.phpt new file mode 100644 index 000000000000..abc2f86c8db7 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/variadic_substituted.phpt @@ -0,0 +1,24 @@ +--TEST-- +Parametric LSP: variadic parameter substituted with the bound argument +--FILE-- + { + public function acceptAll(T ...$items): int; +} + +class IntAcceptor implements Acceptor { + public function acceptAll(int ...$items): int { return array_sum($items); } +} + +echo (new IntAcceptor)->acceptAll(1, 2, 3, 4), "\n"; + +try { + /** @phpstan-ignore-next-line */ + (new IntAcceptor)->acceptAll(1, "no"); +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +10 +TypeError diff --git a/Zend/tests/generics/inheritance/lsp/violation_extends_return_sibling_of_bound.phpt b/Zend/tests/generics/inheritance/lsp/violation_extends_return_sibling_of_bound.phpt new file mode 100644 index 000000000000..e9a493b12132 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_extends_return_sibling_of_bound.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: child of `extends A` cannot override return type T with a sibling of the bound (Cat) +--FILE-- + { + public function get(): T {} +} + +class DogChild extends A { + public function get(): Cat { return new Cat(); } +} +?> +--EXPECTF-- +Fatal error: Declaration of DogChild::get(): Cat must be compatible with A::get(): Dog in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_extends_return_transitive.phpt b/Zend/tests/generics/inheritance/lsp/violation_extends_return_transitive.phpt new file mode 100644 index 000000000000..7dc31ae3607e --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_extends_return_transitive.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: violation propagates through a two-level extends chain +--FILE-- + { + abstract public function get(): T; +} + +abstract class B extends A {} + +class C extends B { + public function get(): bool { return true; } +} +?> +--EXPECTF-- +Fatal error: Declaration of C::get(): bool must be compatible with A::get(): int in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_extends_return_unbounded.phpt b/Zend/tests/generics/inheritance/lsp/violation_extends_return_unbounded.phpt new file mode 100644 index 000000000000..035ec8bd0538 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_extends_return_unbounded.phpt @@ -0,0 +1,14 @@ +--TEST-- +Parametric LSP: child of `extends A` cannot override return type T with an unrelated type +--FILE-- + { + public function get(): T {} +} + +class IntChild extends A { + public function get(): bool { return true; } +} +?> +--EXPECTF-- +Fatal error: Declaration of IntChild::get(): bool must be compatible with A::get(): int in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_nullable_T_message.phpt b/Zend/tests/generics/inheritance/lsp/violation_nullable_T_message.phpt new file mode 100644 index 000000000000..93db9b0e7e96 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_nullable_T_message.phpt @@ -0,0 +1,14 @@ +--TEST-- +Parametric LSP: error message for a ?T return violation shows the substituted nullable form +--FILE-- + { + public function f(): ?T {} +} + +class B extends A { + public function f(): bool { return true; } +} +?> +--EXPECTF-- +Fatal error: Declaration of B::f(): bool must be compatible with A::f(): ?int in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_param_too_specific.phpt b/Zend/tests/generics/inheritance/lsp/violation_param_too_specific.phpt new file mode 100644 index 000000000000..832a8c92bee0 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_param_too_specific.phpt @@ -0,0 +1,17 @@ +--TEST-- +Parametric LSP: child param more specific than the bound argument is rejected (contravariance) +--FILE-- + { + public function eat(T $x): void; +} + +class TooSpecificEater implements Eater { + public function eat(Dog $x): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of TooSpecificEater::eat(Dog $x): void must be compatible with Eater::eat(Animal $x): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_return_too_general.phpt b/Zend/tests/generics/inheritance/lsp/violation_return_too_general.phpt new file mode 100644 index 000000000000..bfd442d0c484 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_return_too_general.phpt @@ -0,0 +1,17 @@ +--TEST-- +Parametric LSP: child return more general than the bound argument is rejected (covariance) +--FILE-- + { + public function produce(): T; +} + +class TooGeneralProducer implements Producer { + public function produce(): Animal { return new Animal; } +} +?> +--EXPECTF-- +Fatal error: Declaration of TooGeneralProducer::produce(): Animal must be compatible with Producer::produce(): Dog in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/inherited_concrete_unchanged.phpt b/Zend/tests/generics/inheritance/method/inherited_concrete_unchanged.phpt new file mode 100644 index 000000000000..9b08820032dc --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_concrete_unchanged.phpt @@ -0,0 +1,16 @@ +--TEST-- +Inherited methods: concretely typed parameters and returns are unaffected by substitution +--FILE-- + { + public function bare(int $x): string { return (string)$x; } +} + +class IntHelper extends Helper {} + +$rm = (new ReflectionClass('IntHelper'))->getMethod('bare'); +echo $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +int -> string diff --git a/Zend/tests/generics/inheritance/method/inherited_from_interface.phpt b/Zend/tests/generics/inheritance/method/inherited_from_interface.phpt new file mode 100644 index 000000000000..f01ef9796d5d --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_from_interface.phpt @@ -0,0 +1,15 @@ +--TEST-- +Inherited methods: abstract method inherited from a generic interface is substituted on the implementing class +--FILE-- + { + public function unwrap(): T; +} + +abstract class IntBoxImpl implements Box {} + +$rm = (new ReflectionClass('IntBoxImpl'))->getMethod('unwrap'); +echo $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +int diff --git a/Zend/tests/generics/inheritance/method/inherited_method_with_named_with_args_return.phpt b/Zend/tests/generics/inheritance/method/inherited_method_with_named_with_args_return.phpt new file mode 100644 index 000000000000..9ceac319adf7 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_method_with_named_with_args_return.phpt @@ -0,0 +1,20 @@ +--TEST-- +Inherited methods: substituted clone keeps the erased class-name view when the return type wraps a method-level type parameter +--FILE-- + { + private mixed $value; + public function __construct(mixed $val) { $this->value = $val; } + public static function create(O $value): Box { return new self($value); } +} + +class StringBox extends Box {} + +$b = StringBox::create("hello"); +var_dump($b instanceof Box); +$rm = (new ReflectionMethod(StringBox::class, 'create')); +echo "return type: ", (string)$rm->getReturnType(), "\n"; +?> +--EXPECT-- +bool(true) +return type: Box diff --git a/Zend/tests/generics/inheritance/method/inherited_signature_substituted.phpt b/Zend/tests/generics/inheritance/method/inherited_signature_substituted.phpt new file mode 100644 index 000000000000..c64ffc005ad9 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_signature_substituted.phpt @@ -0,0 +1,21 @@ +--TEST-- +Inherited methods: param and return types are substituted on a class extending a generic parent +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class IntBase extends Base {} +class StrBase extends Base {} + +foreach (['IntBase', 'StrBase'] as $cn) { + $rm = (new ReflectionClass($cn))->getMethod('take'); + echo "$cn::take: ", + $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; +} +?> +--EXPECT-- +IntBase::take: int -> int +StrBase::take: string -> string diff --git a/Zend/tests/generics/inheritance/method/inherited_signature_transitive.phpt b/Zend/tests/generics/inheritance/method/inherited_signature_transitive.phpt new file mode 100644 index 000000000000..42d27b7d7ffb --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_signature_transitive.phpt @@ -0,0 +1,17 @@ +--TEST-- +Inherited methods: substitution composes through a generic intermediate class +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class Mid extends Base {} +class Leaf extends Mid {} + +$rm = (new ReflectionClass('Leaf'))->getMethod('take'); +echo $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +float -> float diff --git a/Zend/tests/generics/inheritance/method/inherited_variadic_substituted.phpt b/Zend/tests/generics/inheritance/method/inherited_variadic_substituted.phpt new file mode 100644 index 000000000000..c2dbc918e450 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_variadic_substituted.phpt @@ -0,0 +1,15 @@ +--TEST-- +Inherited methods: variadic parameter typed T is substituted on the child class +--FILE-- + { + public function all(T ...$xs): int { return count($xs); } +} + +class IntBase extends Base {} + +$rm = (new ReflectionClass('IntBase'))->getMethod('all'); +echo "param: ", $rm->getParameters()[0]->getType()->getName(), "\n"; +?> +--EXPECT-- +param: int diff --git a/Zend/tests/generics/inheritance/property/extends_concrete_arg.phpt b/Zend/tests/generics/inheritance/property/extends_concrete_arg.phpt new file mode 100644 index 000000000000..1f685d3f0b00 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_concrete_arg.phpt @@ -0,0 +1,27 @@ +--TEST-- +Property substitution: class extends generic parent with concrete arg substitutes property type +--FILE-- + { + public T $val; +} + +class IntBox extends Box {} + +$rp = (new ReflectionClass('IntBox'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; + +$ib = new IntBox; +$ib->val = 42; +echo $ib->val, "\n"; + +try { + $ib->val = []; +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +int +42 +TypeError diff --git a/Zend/tests/generics/inheritance/property/extends_concrete_property_unchanged.phpt b/Zend/tests/generics/inheritance/property/extends_concrete_property_unchanged.phpt new file mode 100644 index 000000000000..74b04b7f8e72 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_concrete_property_unchanged.phpt @@ -0,0 +1,17 @@ +--TEST-- +Property substitution: parent property already typed with a concrete type is unchanged +--FILE-- + { + public int $count = 0; + public T $val; +} + +class StrBox extends Box {} + +$rcCount = (new ReflectionClass('StrBox'))->getProperty('count'); +$rcVal = (new ReflectionClass('StrBox'))->getProperty('val'); +echo $rcCount->getType()->getName(), "/", $rcVal->getType()->getName(), "\n"; +?> +--EXPECT-- +int/string diff --git a/Zend/tests/generics/inheritance/property/extends_default_arg.phpt b/Zend/tests/generics/inheritance/property/extends_default_arg.phpt new file mode 100644 index 000000000000..2e809d906fc3 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_default_arg.phpt @@ -0,0 +1,23 @@ +--TEST-- +Property substitution: extends without args uses parent's parameter default +--FILE-- + { + public T $val; +} + +class Bag extends Box {} + +$rp = (new ReflectionClass('Bag'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; + +$b = new Bag; +$b->val = 7; +echo $b->val, "\n"; + +try { $b->val = "x"; } catch (TypeError) { echo "TypeError\n"; } +?> +--EXPECT-- +int +7 +TypeError diff --git a/Zend/tests/generics/inheritance/property/extends_get_and_set_hook.phpt b/Zend/tests/generics/inheritance/property/extends_get_and_set_hook.phpt new file mode 100644 index 000000000000..7c71f594849a --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_get_and_set_hook.phpt @@ -0,0 +1,27 @@ +--TEST-- +Property substitution: get and set hook signatures are both substituted on a class extending a generic parent +--FILE-- + { + public T $val { + get => $this->val; + set => $this->val = $value; + } + public function __construct(T $v) { $this->val = $v; } +} + +class IntBox extends Box {} +class StrBox extends Box {} + +foreach (['IntBox', 'StrBox'] as $cn) { + $rp = (new ReflectionClass($cn))->getProperty('val'); + $g = $rp->getHook(PropertyHookType::Get); + $s = $rp->getHook(PropertyHookType::Set); + echo "$cn: prop=", $rp->getType()->getName(), + " get=", $g->getReturnType()->getName(), + " set=", $s->getParameters()[0]->getType()->getName(), "\n"; +} +?> +--EXPECT-- +IntBox: prop=int get=int set=int +StrBox: prop=string get=string set=string diff --git a/Zend/tests/generics/inheritance/property/extends_get_hook.phpt b/Zend/tests/generics/inheritance/property/extends_get_hook.phpt new file mode 100644 index 000000000000..3e8f46d8bbba --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_get_hook.phpt @@ -0,0 +1,24 @@ +--TEST-- +Property substitution: get hook return type is substituted on a class extending a generic parent +--FILE-- + { + public T $val { get => $this->val; } + public function __construct(T $v) { $this->val = $v; } +} + +class IntBox extends Box {} + +$rp = (new ReflectionClass('IntBox'))->getProperty('val'); +echo "prop type: ", $rp->getType()->getName(), "\n"; + +$get = $rp->getHook(PropertyHookType::Get); +echo "get return: ", $get->getReturnType()->getName(), "\n"; + +$ib = new IntBox(42); +echo "value: ", $ib->val, "\n"; +?> +--EXPECT-- +prop type: int +get return: int +value: 42 diff --git a/Zend/tests/generics/inheritance/property/extends_get_hook_transitive.phpt b/Zend/tests/generics/inheritance/property/extends_get_hook_transitive.phpt new file mode 100644 index 000000000000..98b021c591fb --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_get_hook_transitive.phpt @@ -0,0 +1,25 @@ +--TEST-- +Property substitution: get hook return type substitutes through a generic intermediate class +--FILE-- + { + public T $val { get => $this->val; } + public function __construct(T $v) { $this->val = $v; } +} + +class Mid extends Box {} +class Leaf extends Mid {} + +$rp = (new ReflectionClass('Leaf'))->getProperty('val'); +echo "prop type: ", $rp->getType()->getName(), "\n"; + +$get = $rp->getHook(PropertyHookType::Get); +echo "get return: ", $get->getReturnType()->getName(), "\n"; + +$leaf = new Leaf(3.14); +echo "value: ", $leaf->val, "\n"; +?> +--EXPECT-- +prop type: float +get return: float +value: 3.14 diff --git a/Zend/tests/generics/inheritance/property/extends_set_hook.phpt b/Zend/tests/generics/inheritance/property/extends_set_hook.phpt new file mode 100644 index 000000000000..a06399be4801 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_set_hook.phpt @@ -0,0 +1,32 @@ +--TEST-- +Property substitution: set hook value parameter type is substituted on a class extending a generic parent +--FILE-- + { + public T $val { set => $this->val = $value; } + public function __construct(T $v) { $this->val = $v; } +} + +class IntBox extends Box {} + +$rp = (new ReflectionClass('IntBox'))->getProperty('val'); +echo "prop type: ", $rp->getType()->getName(), "\n"; + +$set = $rp->getHook(PropertyHookType::Set); +echo "set param: ", $set->getParameters()[0]->getType()->getName(), "\n"; + +$ib = new IntBox(5); +$ib->val = 10; +echo $ib->val, "\n"; + +try { + $ib->val = "no"; +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +prop type: int +set param: int +10 +TypeError diff --git a/Zend/tests/generics/inheritance/property/extends_transitive.phpt b/Zend/tests/generics/inheritance/property/extends_transitive.phpt new file mode 100644 index 000000000000..7f9298efd8e4 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_transitive.phpt @@ -0,0 +1,24 @@ +--TEST-- +Property substitution: substitution composes through a generic intermediate class +--FILE-- + { + public T $val; +} + +class Mid extends Box {} +class Leaf extends Mid {} + +$rp = (new ReflectionClass('Leaf'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; + +$leaf = new Leaf; +$leaf->val = 3.14; +echo $leaf->val, "\n"; + +try { $leaf->val = "x"; } catch (TypeError) { echo "TypeError\n"; } +?> +--EXPECT-- +float +3.14 +TypeError diff --git a/Zend/tests/generics/inheritance/property/extends_two_classes_distinct_types.phpt b/Zend/tests/generics/inheritance/property/extends_two_classes_distinct_types.phpt new file mode 100644 index 000000000000..cea7bcfdd921 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_two_classes_distinct_types.phpt @@ -0,0 +1,25 @@ +--TEST-- +Property substitution: two classes extending the same generic parent get distinct property types +--FILE-- + { + public T $val; +} + +class IntBox extends Box {} +class StrBox extends Box {} + +$rcInt = (new ReflectionClass('IntBox'))->getProperty('val'); +$rcStr = (new ReflectionClass('StrBox'))->getProperty('val'); +echo $rcInt->getType()->getName(), "/", $rcStr->getType()->getName(), "\n"; + +$ib = new IntBox; $ib->val = 1; +$sb = new StrBox; $sb->val = "x"; + +try { $ib->val = "no"; } catch (TypeError) { echo "TypeError IntBox\n"; } +try { $sb->val = []; } catch (TypeError) { echo "TypeError StrBox\n"; } +?> +--EXPECT-- +int/string +TypeError IntBox +TypeError StrBox diff --git a/Zend/tests/generics/inheritance/self_and_static_with_generics.phpt b/Zend/tests/generics/inheritance/self_and_static_with_generics.phpt new file mode 100644 index 000000000000..ac66243bdf06 --- /dev/null +++ b/Zend/tests/generics/inheritance/self_and_static_with_generics.phpt @@ -0,0 +1,36 @@ +--TEST-- +Inheritance: self / static in a generic ancestor compose with parametric LSP +--FILE-- + { + abstract public function build(): T; + public static function make(): static { return new static; } +} + +class IntBuilder extends Builder { + public function build(): int { return 42; } +} + +interface Box { + public function unwrap(): T; + public function self_clone(): self; +} + +class IntBox implements Box { + public function __construct(private int $v) {} + public function unwrap(): int { return $this->v; } + public function self_clone(): self { return new self($this->v); } +} + +$ib = IntBuilder::make(); +echo get_class($ib), ":", $ib->build(), "\n"; + +$b = new IntBox(7); +echo $b->unwrap(), "\n"; +$c = $b->self_clone(); +echo get_class($c), ":", $c->unwrap(), "\n"; +?> +--EXPECT-- +IntBuilder:42 +7 +IntBox:7 diff --git a/Zend/tests/generics/limit/arg_list_return_type_128_rejected.phpt b/Zend/tests/generics/limit/arg_list_return_type_128_rejected.phpt new file mode 100644 index 000000000000..bcfc3cf2cf14 --- /dev/null +++ b/Zend/tests/generics/limit/arg_list_return_type_128_rejected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Generics: 127 type arguments on a function return type is OK; 128 is a compile error +--FILE-- + {}"); +echo "OK\n"; + +$N = implode(",", array_fill(0, 128, "int")); +eval("function f128(): Box<{$N}> {}"); +?> +--EXPECTF-- +OK + +Fatal error: Cannot specify more than 127 generic type arguments (got 128) in %s : eval()'d code on line %d diff --git a/Zend/tests/generics/limit/param_list_class_128_rejected.phpt b/Zend/tests/generics/limit/param_list_class_128_rejected.phpt new file mode 100644 index 000000000000..2897d63b7d17 --- /dev/null +++ b/Zend/tests/generics/limit/param_list_class_128_rejected.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generics: 127 type parameters on a class is OK; 128 is a compile error +--FILE-- + "T{$i}", range(0, 126))); +eval("class C127<{$names127}> {}"); +echo "OK\n"; + +$names128 = implode(",", array_map(fn($i) => "T{$i}", range(0, 127))); +eval("class C128<{$names128}> {}"); +?> +--EXPECTF-- +OK + +Fatal error: Cannot declare more than 127 generic type parameters (got 128) in %s : eval()'d code on line %d diff --git a/Zend/tests/generics/limit/turbofish_function_call_128_rejected.phpt b/Zend/tests/generics/limit/turbofish_function_call_128_rejected.phpt new file mode 100644 index 000000000000..7ea201aada64 --- /dev/null +++ b/Zend/tests/generics/limit/turbofish_function_call_128_rejected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Generics: 127 type arguments on a function call turbofish is OK; 128 is a compile error +--FILE-- +(); }"); +echo "OK\n"; + +$N = implode(",", array_fill(0, 128, "int")); +eval("function g128() { f::<{$N}>(); }"); +?> +--EXPECTF-- +OK + +Fatal error: Cannot specify more than 127 generic type arguments (got 128) in %s : eval()'d code on line %d diff --git a/Zend/tests/generics/reflection/namespace_relative_extends_implements_uses.phpt b/Zend/tests/generics/reflection/namespace_relative_extends_implements_uses.phpt new file mode 100644 index 000000000000..87bb2d4aaca4 --- /dev/null +++ b/Zend/tests/generics/reflection/namespace_relative_extends_implements_uses.phpt @@ -0,0 +1,64 @@ +--TEST-- +Reflection: namespace-relative `extends`/`implements`/`use` keep their generic arguments in the side table +--FILE-- + {} +interface Iface<+T> {} +trait Mixin<+T> {} + +class FqnExt extends \NSRelExtImpUse\Base {} +class NsExt extends namespace\Base {} +class RelExt extends Base {} + +class FqnImpl implements \NSRelExtImpUse\Iface {} +class NsImpl implements namespace\Iface {} +class RelImpl implements Iface {} + +class FqnUse { use \NSRelExtImpUse\Mixin; } +class NsUse { use namespace\Mixin; } +class RelUse { use Mixin; } + +$singles = [ + 'extends FQN' => (new \ReflectionClass(FqnExt::class))->getGenericArgumentsForParentClass(), + 'extends ns' => (new \ReflectionClass(NsExt::class))->getGenericArgumentsForParentClass(), + 'extends rel' => (new \ReflectionClass(RelExt::class))->getGenericArgumentsForParentClass(), + 'use FQN' => (new \ReflectionClass(FqnUse::class))->getGenericArgumentsForUsedTrait(Mixin::class), + 'use ns' => (new \ReflectionClass(NsUse::class))->getGenericArgumentsForUsedTrait(Mixin::class), + 'use rel' => (new \ReflectionClass(RelUse::class))->getGenericArgumentsForUsedTrait(Mixin::class), +]; + +foreach ($singles as $label => $args) { + $rendered = array_map(static fn(\ReflectionNamedType $t): string => $t->getName(), $args); + printf("%-12s count=%d args=[%s]\n", $label, count($args), implode(', ', $rendered)); +} + +$plural = [ + 'impl FQN' => (new \ReflectionClass(FqnImpl::class))->getGenericArgumentsForParentInterface(Iface::class), + 'impl ns' => (new \ReflectionClass(NsImpl::class))->getGenericArgumentsForParentInterface(Iface::class), + 'impl rel' => (new \ReflectionClass(RelImpl::class))->getGenericArgumentsForParentInterface(Iface::class), +]; + +foreach ($plural as $label => $bindings) { + $rendered = array_map( + static fn(array $b): string => '[' . implode(', ', array_map( + static fn(\ReflectionNamedType $t): string => $t->getName(), + $b, + )) . ']', + $bindings, + ); + printf("%-12s bindings=%d sets=[%s]\n", $label, count($bindings), implode(', ', $rendered)); +} +?> +--EXPECT-- +extends FQN count=1 args=[int] +extends ns count=1 args=[int] +extends rel count=1 args=[int] +use FQN count=1 args=[int] +use ns count=1 args=[int] +use rel count=1 args=[int] +impl FQN bindings=1 sets=[[int]] +impl ns bindings=1 sets=[[int]] +impl rel bindings=1 sets=[[int]] diff --git a/Zend/tests/generics/scoping/closure_returned_visible_in_reflection.phpt b/Zend/tests/generics/scoping/closure_returned_visible_in_reflection.phpt new file mode 100644 index 000000000000..8ceeac6f49a9 --- /dev/null +++ b/Zend/tests/generics/scoping/closure_returned_visible_in_reflection.phpt @@ -0,0 +1,13 @@ +--TEST-- +Scoping: nested closure has its own ReflectionFunction view +--FILE-- +(): Closure { + return function (T $y): T { return $y; }; +} +$cl = f(); +$r = new ReflectionFunction($cl); +echo $r->getReturnType()->__toString(), "\n"; +?> +--EXPECT-- +object diff --git a/Zend/tests/generics/scoping/extends_clause.phpt b/Zend/tests/generics/scoping/extends_clause.phpt new file mode 100644 index 000000000000..d9839bf3d48f --- /dev/null +++ b/Zend/tests/generics/scoping/extends_clause.phpt @@ -0,0 +1,11 @@ +--TEST-- +Scoping: T is visible in extends clause +--FILE-- + {} +class Derived extends Base {} +$rc = new ReflectionClass('Derived'); +echo $rc->getParentClass()->getName(), "\n"; +?> +--EXPECT-- +Base diff --git a/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt b/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt new file mode 100644 index 000000000000..09c19fc79aea --- /dev/null +++ b/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt @@ -0,0 +1,11 @@ +--TEST-- +Scoping: a file-scope class does NOT shadow a generic type parameter +--FILE-- +(): void { + new T(); +} +?> +--EXPECTF-- +Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d diff --git a/Zend/tests/generics/scoping/fq_name_bypasses_type_param.phpt b/Zend/tests/generics/scoping/fq_name_bypasses_type_param.phpt new file mode 100644 index 000000000000..032fad8b69b4 --- /dev/null +++ b/Zend/tests/generics/scoping/fq_name_bypasses_type_param.phpt @@ -0,0 +1,15 @@ +--TEST-- +Scoping: a fully qualified name `\T` bypasses the type-parameter rule +--FILE-- +(): int { + $x = new \T(); + return $x->v; +} +echo f(), "\n"; +?> +--EXPECT-- +5 diff --git a/Zend/tests/generics/scoping/implements_clause.phpt b/Zend/tests/generics/scoping/implements_clause.phpt new file mode 100644 index 000000000000..44de9db4b292 --- /dev/null +++ b/Zend/tests/generics/scoping/implements_clause.phpt @@ -0,0 +1,11 @@ +--TEST-- +Scoping: T is visible in implements clause +--FILE-- + {} +class Sink implements IConsumer {} +$rc = new ReflectionClass('Sink'); +echo $rc->getInterfaceNames()[0], "\n"; +?> +--EXPECT-- +IConsumer diff --git a/Zend/tests/generics/scoping/inner_class_shadowing_is_case_insensitive.phpt b/Zend/tests/generics/scoping/inner_class_shadowing_is_case_insensitive.phpt new file mode 100644 index 000000000000..0261f6a21b33 --- /dev/null +++ b/Zend/tests/generics/scoping/inner_class_shadowing_is_case_insensitive.phpt @@ -0,0 +1,15 @@ +--TEST-- +Scoping: inner-class shadowing of a type parameter is case-insensitive +--FILE-- +(): void { + class t { + public int $v = 9; + } + $x = new T(); + echo $x->v, "\n"; +} +f(); +?> +--EXPECT-- +9 diff --git a/Zend/tests/generics/scoping/inner_class_shadows_in_instanceof.phpt b/Zend/tests/generics/scoping/inner_class_shadows_in_instanceof.phpt new file mode 100644 index 000000000000..a4637ae93909 --- /dev/null +++ b/Zend/tests/generics/scoping/inner_class_shadows_in_instanceof.phpt @@ -0,0 +1,14 @@ +--TEST-- +Scoping: inner class shadows the type parameter for `instanceof` +--FILE-- +($x): bool { + class T {} + return $x instanceof T; +} +$called = false; +$result = f(new stdClass); +var_dump($result); +?> +--EXPECT-- +bool(false) diff --git a/Zend/tests/generics/scoping/inner_class_shadows_type_param.phpt b/Zend/tests/generics/scoping/inner_class_shadows_type_param.phpt new file mode 100644 index 000000000000..813387557be2 --- /dev/null +++ b/Zend/tests/generics/scoping/inner_class_shadows_type_param.phpt @@ -0,0 +1,15 @@ +--TEST-- +Scoping: a class declared inside a generic body shadows the type parameter +--FILE-- +(): void { + class T { + public int $v = 7; + } + $x = new T(); + echo $x->v, "\n"; +} +f(); +?> +--EXPECT-- +7 diff --git a/Zend/tests/generics/scoping/left_to_right_param_bounds.phpt b/Zend/tests/generics/scoping/left_to_right_param_bounds.phpt new file mode 100644 index 000000000000..479039281346 --- /dev/null +++ b/Zend/tests/generics/scoping/left_to_right_param_bounds.phpt @@ -0,0 +1,12 @@ +--TEST-- +Scoping: type parameters resolve in subsequent bounds (left to right) +--FILE-- + { + public U $val; +} +$rt = (new ReflectionClass('Box'))->getProperty('val')->getType(); +echo $rt->getName(), "\n"; +?> +--EXPECT-- +object diff --git a/Zend/tests/generics/scoping/method_introduces_own.phpt b/Zend/tests/generics/scoping/method_introduces_own.phpt new file mode 100644 index 000000000000..5c3ff1c900f2 --- /dev/null +++ b/Zend/tests/generics/scoping/method_introduces_own.phpt @@ -0,0 +1,18 @@ +--TEST-- +Scoping: method introduces its own type parameter +--FILE-- + { + public function morph(T $x, U $y): U { return $y; } +} +$rm = (new ReflectionClass('Box'))->getMethod('morph'); +echo $rm->getParameters()[0]->getType()->getName(), "\n"; +echo $rm->getParameters()[1]->getType()->getName(), "\n"; +echo $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +A +B +B diff --git a/Zend/tests/generics/scoping/method_param_does_not_leak.phpt b/Zend/tests/generics/scoping/method_param_does_not_leak.phpt new file mode 100644 index 000000000000..ec5bd29b0eb5 --- /dev/null +++ b/Zend/tests/generics/scoping/method_param_does_not_leak.phpt @@ -0,0 +1,20 @@ +--TEST-- +Scoping: method type parameter does not leak to other methods +--FILE-- +(): int { + return 1; + } + public function m2(): T { + return new T; + } +} +$b = new Box; +echo $b->m2()->val, "\n"; +?> +--EXPECT-- +7 diff --git a/Zend/tests/generics/scoping/method_param_type.phpt b/Zend/tests/generics/scoping/method_param_type.phpt new file mode 100644 index 000000000000..3be048c7b36e --- /dev/null +++ b/Zend/tests/generics/scoping/method_param_type.phpt @@ -0,0 +1,13 @@ +--TEST-- +Scoping: T from class scope is visible in method parameter types +--FILE-- + { + public function set(T $x): void {} +} +$rm = (new ReflectionClass('Box'))->getMethod('set'); +echo $rm->getParameters()[0]->getType()->getName(), "\n"; +?> +--EXPECT-- +Iface diff --git a/Zend/tests/generics/scoping/method_return_type.phpt b/Zend/tests/generics/scoping/method_return_type.phpt new file mode 100644 index 000000000000..6951dcc9e65d --- /dev/null +++ b/Zend/tests/generics/scoping/method_return_type.phpt @@ -0,0 +1,13 @@ +--TEST-- +Scoping: T from class scope is visible in method return types +--FILE-- + { + public function get(): T { return new Animal; } +} +$rm = (new ReflectionClass('Container'))->getMethod('get'); +echo $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Animal diff --git a/Zend/tests/generics/scoping/nested_anon_class_captures.phpt b/Zend/tests/generics/scoping/nested_anon_class_captures.phpt new file mode 100644 index 000000000000..b7dd6bd3769f --- /dev/null +++ b/Zend/tests/generics/scoping/nested_anon_class_captures.phpt @@ -0,0 +1,21 @@ +--TEST-- +Scoping: nested anonymous class captures enclosing type parameter +--FILE-- +(T $x): object { + return new class($x) { + public T $val; + public function __construct(T $x) { $this->val = $x; } + public function get(): T { return $this->val; } + }; +} +class Dog implements Animal {} +$obj = makeAnimalBox(new Dog); +$r = new ReflectionClass($obj); +echo $r->getProperty('val')->getType()->getName(), "\n"; +echo $r->getMethod('get')->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Animal +Animal diff --git a/Zend/tests/generics/scoping/nested_arrow_captures.phpt b/Zend/tests/generics/scoping/nested_arrow_captures.phpt new file mode 100644 index 000000000000..1dce854b025d --- /dev/null +++ b/Zend/tests/generics/scoping/nested_arrow_captures.phpt @@ -0,0 +1,15 @@ +--TEST-- +Scoping: nested arrow function captures enclosing type parameter +--FILE-- +(): Closure { + return fn(T $y): T => $y; +} +$cl = f(); +$r = new ReflectionFunction($cl); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +int +int diff --git a/Zend/tests/generics/scoping/nested_closure_captures.phpt b/Zend/tests/generics/scoping/nested_closure_captures.phpt new file mode 100644 index 000000000000..d0fcf8ec4118 --- /dev/null +++ b/Zend/tests/generics/scoping/nested_closure_captures.phpt @@ -0,0 +1,16 @@ +--TEST-- +Scoping: nested closure captures enclosing function's type parameter +--FILE-- +(T $x): Closure { + return function (T $y): T { return $y; }; +} +class Foo {} +$cl = f(new Foo); +$r = new ReflectionFunction($cl); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +object +object diff --git a/Zend/tests/generics/scoping/property_type.phpt b/Zend/tests/generics/scoping/property_type.phpt new file mode 100644 index 000000000000..898ecd1d96b9 --- /dev/null +++ b/Zend/tests/generics/scoping/property_type.phpt @@ -0,0 +1,12 @@ +--TEST-- +Scoping: T from class scope is visible in property types +--FILE-- + { + public T $value; +} +$rt = (new ReflectionClass('Box'))->getProperty('value')->getType(); +echo $rt->getName(), "\n"; +?> +--EXPECT-- +object diff --git a/Zend/tests/generics/scoping/trait_use_clause.phpt b/Zend/tests/generics/scoping/trait_use_clause.phpt new file mode 100644 index 000000000000..7f3ff0b30690 --- /dev/null +++ b/Zend/tests/generics/scoping/trait_use_clause.phpt @@ -0,0 +1,17 @@ +--TEST-- +Scoping: T is visible in trait use clause (no compile error) +--FILE-- + { + public function tag(X $x): X { return $x; } +} +class Box { + use Holder; +} +echo (new ReflectionClass('Box'))->getMethod('tag')->getReturnType()->getName(), "\n"; +$b = new Box; +echo get_class($b), "\n"; +?> +--EXPECT-- +mixed +Box diff --git a/Zend/tests/generics/syntax/args_dnf.phpt b/Zend/tests/generics/syntax/args_dnf.phpt new file mode 100644 index 000000000000..61829c694c59 --- /dev/null +++ b/Zend/tests/generics/syntax/args_dnf.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generic syntax: type argument is DNF +--FILE-- + $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +$arg = $pt->getGenericArguments()[0]; +echo get_class($arg), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/syntax/args_intersection.phpt b/Zend/tests/generics/syntax/args_intersection.phpt new file mode 100644 index 000000000000..5cd01bda2a66 --- /dev/null +++ b/Zend/tests/generics/syntax/args_intersection.phpt @@ -0,0 +1,14 @@ +--TEST-- +Generic syntax: type argument is an intersection +--FILE-- + $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +$arg = $pt->getGenericArguments()[0]; +echo get_class($arg), "\n"; +?> +--EXPECT-- +ReflectionIntersectionType diff --git a/Zend/tests/generics/syntax/args_union.phpt b/Zend/tests/generics/syntax/args_union.phpt new file mode 100644 index 000000000000..206b88266774 --- /dev/null +++ b/Zend/tests/generics/syntax/args_union.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: type argument is a union +--FILE-- + $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +$arg = $pt->getGenericArguments()[0]; +echo get_class($arg), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/syntax/arrow_fn_decl.phpt b/Zend/tests/generics/syntax/arrow_fn_decl.phpt new file mode 100644 index 000000000000..22790e280438 --- /dev/null +++ b/Zend/tests/generics/syntax/arrow_fn_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: arrow function with type parameters +--FILE-- +(T $x): T => $x; +$r = new ReflectionFunction($f); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/bound_and_default_with_args.phpt b/Zend/tests/generics/syntax/bound_and_default_with_args.phpt new file mode 100644 index 000000000000..620484d0bf05 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_and_default_with_args.phpt @@ -0,0 +1,20 @@ +--TEST-- +Generic syntax: bound and default both carry type arguments, closing >> +--FILE-- + {} + +// class A has a generic parameter T with bound B and default B +class A=B> {} + +$p = (new ReflectionClass('A'))->getGenericParameters()[0]; + +$b = $p->getBound(); +echo "bound: ", $b->getName(), "<", $b->getGenericArguments()[0]->getName(), ">\n"; + +$d = $p->getDefault(); +echo "default: ", $d->getName(), "<", $d->getGenericArguments()[0]->getName(), ">\n"; +?> +--EXPECT-- +bound: B +default: B diff --git a/Zend/tests/generics/syntax/bound_dnf.phpt b/Zend/tests/generics/syntax/bound_dnf.phpt new file mode 100644 index 000000000000..7d65ae2dcbd7 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_dnf.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: bound is a DNF type +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +echo get_class($p->getBound()), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/syntax/bound_intersection.phpt b/Zend/tests/generics/syntax/bound_intersection.phpt new file mode 100644 index 000000000000..f73eeb2e4357 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_intersection.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: bound is an intersection type +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +echo get_class($p->getBound()), "\n"; +?> +--EXPECT-- +ReflectionIntersectionType diff --git a/Zend/tests/generics/syntax/bound_single_class.phpt b/Zend/tests/generics/syntax/bound_single_class.phpt new file mode 100644 index 000000000000..d6000909b072 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_single_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: bound on a single class type +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +var_dump($p->hasBound()); +echo $p->getBound()->getName(), "\n"; +?> +--EXPECT-- +bool(true) +Animal diff --git a/Zend/tests/generics/syntax/bound_union.phpt b/Zend/tests/generics/syntax/bound_union.phpt new file mode 100644 index 000000000000..2c9c71f9d4b1 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_union.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: bound is a union type +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +$b = $p->getBound(); +echo get_class($b), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/syntax/builtin_self_parent_static.phpt b/Zend/tests/generics/syntax/builtin_self_parent_static.phpt new file mode 100644 index 000000000000..35934d41fa6e --- /dev/null +++ b/Zend/tests/generics/syntax/builtin_self_parent_static.phpt @@ -0,0 +1,20 @@ +--TEST-- +Generic syntax: self, parent, static +--FILE-- + {} + public function p(): parent {} + public function st(): static {} +} +$rc = new ReflectionClass('Foo'); +foreach (['s' => 'self', 'p' => 'parent', 'st' => 'static'] as $m => $expected) { + $rt = $rc->getMethod($m)->getReturnType(); + echo "$m: ", $rt->getName(), " hasArgs=", var_export($rt->hasGenericArguments(), true), "\n"; +} +?> +--EXPECT-- +s: Foo hasArgs=true +p: Base hasArgs=true +st: static hasArgs=true diff --git a/Zend/tests/generics/syntax/catch_with_args.phpt b/Zend/tests/generics/syntax/catch_with_args.phpt new file mode 100644 index 000000000000..bf82150c2660 --- /dev/null +++ b/Zend/tests/generics/syntax/catch_with_args.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: catch with type arguments (args discarded at runtime) +--FILE-- + $e) { + echo "caught: ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +caught: boom diff --git a/Zend/tests/generics/syntax/class_multiple_params.phpt b/Zend/tests/generics/syntax/class_multiple_params.phpt new file mode 100644 index 000000000000..4f66e3173fe2 --- /dev/null +++ b/Zend/tests/generics/syntax/class_multiple_params.phpt @@ -0,0 +1,14 @@ +--TEST-- +Generic syntax: class with multiple type parameters +--FILE-- + {} +$r = new ReflectionClass('Map'); +$ps = $r->getGenericParameters(); +echo count($ps), "\n"; +foreach ($ps as $p) echo $p->getName(), "\n"; +?> +--EXPECT-- +2 +K +V diff --git a/Zend/tests/generics/syntax/class_single_param.phpt b/Zend/tests/generics/syntax/class_single_param.phpt new file mode 100644 index 000000000000..6a5a8bed0cef --- /dev/null +++ b/Zend/tests/generics/syntax/class_single_param.phpt @@ -0,0 +1,14 @@ +--TEST-- +Generic syntax: class with single type parameter +--FILE-- + {} +$r = new ReflectionClass('Box'); +var_dump($r->isGeneric()); +echo count($r->getGenericParameters()), "\n"; +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +1 +T diff --git a/Zend/tests/generics/syntax/closure_decl.phpt b/Zend/tests/generics/syntax/closure_decl.phpt new file mode 100644 index 000000000000..35620b3ae631 --- /dev/null +++ b/Zend/tests/generics/syntax/closure_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: closure with type parameters +--FILE-- +(T $x): T { return $x; }; +$r = new ReflectionFunction($f); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/default_value.phpt b/Zend/tests/generics/syntax/default_value.phpt new file mode 100644 index 000000000000..03f6a867fe3b --- /dev/null +++ b/Zend/tests/generics/syntax/default_value.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: type parameter with default +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +var_dump($p->hasDefault()); +echo $p->getDefault(), "\n"; +?> +--EXPECT-- +bool(true) +mixed diff --git a/Zend/tests/generics/syntax/default_with_nested_args.phpt b/Zend/tests/generics/syntax/default_with_nested_args.phpt new file mode 100644 index 000000000000..8a2af325ff86 --- /dev/null +++ b/Zend/tests/generics/syntax/default_with_nested_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: >>= splitting in default position +--FILE-- +> {} +$p = (new ReflectionClass('C'))->getGenericParameters()[0]; +echo $p, "\n"; +?> +--EXPECT-- +T = MapT diff --git a/Zend/tests/generics/syntax/extends_with_args.phpt b/Zend/tests/generics/syntax/extends_with_args.phpt new file mode 100644 index 000000000000..9a475577183d --- /dev/null +++ b/Zend/tests/generics/syntax/extends_with_args.phpt @@ -0,0 +1,10 @@ +--TEST-- +Generic syntax: extends with type arguments +--FILE-- + {} +class Derived extends Base {} +echo (new ReflectionClass('Derived'))->getParentClass()->getName(), "\n"; +?> +--EXPECT-- +Base diff --git a/Zend/tests/generics/syntax/function_decl.phpt b/Zend/tests/generics/syntax/function_decl.phpt new file mode 100644 index 000000000000..8ba5cde4314a --- /dev/null +++ b/Zend/tests/generics/syntax/function_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: function declaration with type parameters +--FILE-- +(T $x): T { return $x; } +$r = new ReflectionFunction('id'); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/implements_with_args.phpt b/Zend/tests/generics/syntax/implements_with_args.phpt new file mode 100644 index 000000000000..5221dffc4f13 --- /dev/null +++ b/Zend/tests/generics/syntax/implements_with_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: implements with type arguments +--FILE-- + {} +class C implements Iface {} +$r = new ReflectionClass('C'); +foreach ($r->getInterfaceNames() as $n) echo $n, "\n"; +?> +--EXPECT-- +Iface diff --git a/Zend/tests/generics/syntax/instanceof_with_args.phpt b/Zend/tests/generics/syntax/instanceof_with_args.phpt new file mode 100644 index 000000000000..5e303b14bb6a --- /dev/null +++ b/Zend/tests/generics/syntax/instanceof_with_args.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generic syntax: instanceof with type arguments (args discarded at runtime) +--FILE-- +); +var_dump($c instanceof C); +$x = new stdClass; +var_dump($x instanceof C); +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) diff --git a/Zend/tests/generics/syntax/interface_decl.phpt b/Zend/tests/generics/syntax/interface_decl.phpt new file mode 100644 index 000000000000..e0dbb24a6457 --- /dev/null +++ b/Zend/tests/generics/syntax/interface_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: interface with type parameters +--FILE-- + {} +$r = new ReflectionClass('Iter'); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/method_decl.phpt b/Zend/tests/generics/syntax/method_decl.phpt new file mode 100644 index 000000000000..8a76b03588db --- /dev/null +++ b/Zend/tests/generics/syntax/method_decl.phpt @@ -0,0 +1,14 @@ +--TEST-- +Generic syntax: method declaration with type parameters +--FILE-- +(callable $f): U { return $f(); } +} +$r = (new ReflectionClass('C'))->getMethod('map'); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +U diff --git a/Zend/tests/generics/syntax/named_type_args.phpt b/Zend/tests/generics/syntax/named_type_args.phpt new file mode 100644 index 000000000000..91e98d4c6273 --- /dev/null +++ b/Zend/tests/generics/syntax/named_type_args.phpt @@ -0,0 +1,16 @@ +--TEST-- +Generic syntax: type arguments on a named type at use site +--FILE-- + $x): Container { return $x; } +$r = new ReflectionFunction('f'); +$pt = $r->getParameters()[0]->getType(); +echo $pt->getName(), "\n"; +var_dump($pt->hasGenericArguments()); +foreach ($pt->getGenericArguments() as $a) echo $a->getName(), "\n"; +?> +--EXPECT-- +Container +bool(true) +int diff --git a/Zend/tests/generics/syntax/nested_three_levels.phpt b/Zend/tests/generics/syntax/nested_three_levels.phpt new file mode 100644 index 000000000000..9095735c9ae0 --- /dev/null +++ b/Zend/tests/generics/syntax/nested_three_levels.phpt @@ -0,0 +1,21 @@ +--TEST-- +Generic syntax: three levels of nesting (>>> splitting) +--FILE-- +>> { return new A; } +$rt = (new ReflectionFunction('f'))->getReturnType(); +echo $rt->getName(), "\n"; +$arg1 = $rt->getGenericArguments()[0]; +echo $arg1->getName(), "\n"; +$arg2 = $arg1->getGenericArguments()[0]; +echo $arg2->getName(), "\n"; +echo $arg2->getGenericArguments()[0]->getName(), "\n"; +?> +--EXPECT-- +A +B +C +int diff --git a/Zend/tests/generics/syntax/nested_two_levels.phpt b/Zend/tests/generics/syntax/nested_two_levels.phpt new file mode 100644 index 000000000000..f5d3dd38e86f --- /dev/null +++ b/Zend/tests/generics/syntax/nested_two_levels.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generic syntax: two levels of nesting (>> splitting) +--FILE-- +> { return new Outer; } +$rt = (new ReflectionFunction('f'))->getReturnType(); +echo $rt->getName(), "\n"; +$arg = $rt->getGenericArguments()[0]; +echo $arg->getName(), "\n"; +echo $arg->getGenericArguments()[0]->getName(), "\n"; +?> +--EXPECT-- +Outer +Inner +int diff --git a/Zend/tests/generics/syntax/trailing_comma_args.phpt b/Zend/tests/generics/syntax/trailing_comma_args.phpt new file mode 100644 index 000000000000..d7de432dea15 --- /dev/null +++ b/Zend/tests/generics/syntax/trailing_comma_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: trailing comma in type argument list +--FILE-- + $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +echo count($pt->getGenericArguments()), "\n"; +?> +--EXPECT-- +2 diff --git a/Zend/tests/generics/syntax/trailing_comma_params.phpt b/Zend/tests/generics/syntax/trailing_comma_params.phpt new file mode 100644 index 000000000000..3a386ffdeeae --- /dev/null +++ b/Zend/tests/generics/syntax/trailing_comma_params.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: trailing comma in type parameter list +--FILE-- + {} +class Pair {} +echo (new ReflectionClass('Box'))->isGeneric() ? 'ok' : 'fail', "\n"; +echo (new ReflectionClass('Pair'))->isGeneric() ? 'ok' : 'fail', "\n"; +?> +--EXPECT-- +ok +ok diff --git a/Zend/tests/generics/syntax/trait_decl.phpt b/Zend/tests/generics/syntax/trait_decl.phpt new file mode 100644 index 000000000000..32d46440ea81 --- /dev/null +++ b/Zend/tests/generics/syntax/trait_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: trait with type parameters +--FILE-- + {} +$r = new ReflectionClass('Container'); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/trait_use_args.phpt b/Zend/tests/generics/syntax/trait_use_args.phpt new file mode 100644 index 000000000000..acf319fb65ba --- /dev/null +++ b/Zend/tests/generics/syntax/trait_use_args.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generic syntax: trait use with type arguments +--FILE-- + { + public T $tval; +} +class Box { + use Container; +} +$r = new ReflectionClass('Box'); +echo $r->isGeneric() ? 'ok' : 'fail', "\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/syntax/turbofish_complex_args.phpt b/Zend/tests/generics/syntax/turbofish_complex_args.phpt new file mode 100644 index 000000000000..460f6d65fa1b --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_complex_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: turbofish with composite type arguments +--FILE-- +($x) { return $x; } +echo f::(42), "\n"; +echo f::('hi'), "\n"; +?> +--EXPECT-- +42 +hi diff --git a/Zend/tests/generics/syntax/turbofish_fcc.phpt b/Zend/tests/generics/syntax/turbofish_fcc.phpt new file mode 100644 index 000000000000..31080672c5b4 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_fcc.phpt @@ -0,0 +1,24 @@ +--TEST-- +Generic syntax: turbofish on first-class callable creation +--FILE-- +(int $x): int { return $x * 2; } +class C { + public static function s(int $x): int { return $x + 100; } + public function m(int $x): int { return $x - 1; } +} + +$f = f::(...); +echo $f(5), "\n"; + +$g = C::s::(...); +echo $g(5), "\n"; + +$c = new C; +$h = $c->m::(...); +echo $h(5), "\n"; +?> +--EXPECT-- +10 +105 +4 diff --git a/Zend/tests/generics/syntax/turbofish_function_call.phpt b/Zend/tests/generics/syntax/turbofish_function_call.phpt new file mode 100644 index 000000000000..e397c8344643 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_function_call.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: turbofish on function call +--FILE-- +($a, $b) { return $a + $b; } +echo add::(1, 2), "\n"; +echo add::(3, 4), "\n"; +echo add::(5, 6), "\n"; +?> +--EXPECT-- +3 +7 +11 diff --git a/Zend/tests/generics/syntax/turbofish_method_call.phpt b/Zend/tests/generics/syntax/turbofish_method_call.phpt new file mode 100644 index 000000000000..0bfe9f7a2230 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_method_call.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generic syntax: turbofish on instance method call (including nullsafe) +--FILE-- +($x) { return $x * 3; } +} +$c = new C; +echo $c->m::(5), "\n"; +echo $c?->m::(7), "\n"; +$null = null; +var_dump($null?->m::(9)); +?> +--EXPECT-- +15 +21 +NULL diff --git a/Zend/tests/generics/syntax/turbofish_new.phpt b/Zend/tests/generics/syntax/turbofish_new.phpt new file mode 100644 index 000000000000..bc71fbbe37f0 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_new.phpt @@ -0,0 +1,32 @@ +--TEST-- +Generic syntax: turbofish on new with various class-ref forms +--FILE-- + { + public function __construct(public int $v) {} +} + +$b = new Box::(1); +echo $b->v, "\n"; + +$cls = 'Box'; +$b = new $cls::(2); +echo $b->v, "\n"; + +$expr = fn() => 'Box'; +$b = new ($expr())::(3); +echo $b->v, "\n"; + +class Container { + public static function make(): self { + return new self::; + } +} +$c = Container::make(); +echo get_class($c), "\n"; +?> +--EXPECT-- +1 +2 +3 +Container diff --git a/Zend/tests/generics/syntax/turbofish_static_call.phpt b/Zend/tests/generics/syntax/turbofish_static_call.phpt new file mode 100644 index 000000000000..b2a1d74d4b61 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_static_call.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: turbofish on static method call +--FILE-- +($x) { return $x * 2; } +} +echo C::s::(5), "\n"; +echo C::s::(7), "\n"; +?> +--EXPECT-- +10 +14 diff --git a/Zend/tests/generics/syntax/variance_bound_default.phpt b/Zend/tests/generics/syntax/variance_bound_default.phpt new file mode 100644 index 000000000000..55c6460fef55 --- /dev/null +++ b/Zend/tests/generics/syntax/variance_bound_default.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: variance + bound + default combined +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +echo $p, "\n"; +?> +--EXPECT-- ++T : Bar = Bar diff --git a/Zend/tests/generics/syntax/variance_contravariant.phpt b/Zend/tests/generics/syntax/variance_contravariant.phpt new file mode 100644 index 000000000000..97d1b4d07e91 --- /dev/null +++ b/Zend/tests/generics/syntax/variance_contravariant.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: contravariant type parameter (-T) +--FILE-- + {} +$p = (new ReflectionClass('Consumer'))->getGenericParameters()[0]; +echo $p->getName(), "\n"; +echo $p->getVariance()->name, "\n"; +?> +--EXPECT-- +T +Contravariant diff --git a/Zend/tests/generics/syntax/variance_covariant.phpt b/Zend/tests/generics/syntax/variance_covariant.phpt new file mode 100644 index 000000000000..ea3d76f8d2d4 --- /dev/null +++ b/Zend/tests/generics/syntax/variance_covariant.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: covariant type parameter (+T) +--FILE-- + {} +$p = (new ReflectionClass('Producer'))->getGenericParameters()[0]; +echo $p->getName(), "\n"; +echo $p->getVariance()->name, "\n"; +?> +--EXPECT-- +T +Covariant diff --git a/Zend/tests/generics/syntax/variance_invariant_default.phpt b/Zend/tests/generics/syntax/variance_invariant_default.phpt new file mode 100644 index 000000000000..3fd243c44a06 --- /dev/null +++ b/Zend/tests/generics/syntax/variance_invariant_default.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: type parameter without variance is invariant +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +var_dump($p->getVariance() === ReflectionGenericVariance::Invariant); +echo $p->getVariance()->name, "\n"; +?> +--EXPECT-- +bool(true) +Invariant diff --git a/Zend/tests/generics/traits/abstract_method_implemented.phpt b/Zend/tests/generics/traits/abstract_method_implemented.phpt new file mode 100644 index 000000000000..269e70b752a1 --- /dev/null +++ b/Zend/tests/generics/traits/abstract_method_implemented.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: an abstract trait method using T accepts an implementer matching the substituted type +--FILE-- + { + public abstract function foo(T $v): T; +} + +class A { + use Thing; + public function foo(string $v): string { return $v; } +} + +echo (new A)->foo("hi"), "\n"; +?> +--EXPECT-- +hi diff --git a/Zend/tests/generics/traits/abstract_method_two_classes.phpt b/Zend/tests/generics/traits/abstract_method_two_classes.phpt new file mode 100644 index 000000000000..1e5b4e67af64 --- /dev/null +++ b/Zend/tests/generics/traits/abstract_method_two_classes.phpt @@ -0,0 +1,24 @@ +--TEST-- +Traits: two classes using the same generic trait can implement the abstract method at distinct substituted types +--FILE-- + { + public abstract function foo(T $v): T; +} + +class StrThing { + use Thing; + public function foo(string $v): string { return $v; } +} + +class IntThing { + use Thing; + public function foo(int $v): int { return $v; } +} + +echo (new StrThing)->foo("a"), "\n"; +echo (new IntThing)->foo(7), "\n"; +?> +--EXPECT-- +a +7 diff --git a/Zend/tests/generics/traits/abstract_method_violates.phpt b/Zend/tests/generics/traits/abstract_method_violates.phpt new file mode 100644 index 000000000000..9b7179dce1fd --- /dev/null +++ b/Zend/tests/generics/traits/abstract_method_violates.phpt @@ -0,0 +1,15 @@ +--TEST-- +Traits: an abstract trait method using T rejects an implementer with the wrong substituted type +--FILE-- + { + public abstract function foo(T $v): T; +} + +class A { + use Thing; + public function foo(int $v): int { return $v; } +} +?> +--EXPECTF-- +Fatal error: Declaration of A::foo(int $v): int must be compatible with Thing::foo(string $v): string in %s on line %d diff --git a/Zend/tests/generics/traits/basic_trait.phpt b/Zend/tests/generics/traits/basic_trait.phpt new file mode 100644 index 000000000000..7f459bb5f002 --- /dev/null +++ b/Zend/tests/generics/traits/basic_trait.phpt @@ -0,0 +1,18 @@ +--TEST-- +Traits: trait declares its own type parameter +--FILE-- + { + public T $value; + public function get(): T { return $this->value; } +} +class Box { + use Holder; +} +$rc = new ReflectionClass('Box'); +echo $rc->getProperty('value')->getType()->getName(), "\n"; +echo $rc->getMethod('get')->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +object +object diff --git a/Zend/tests/generics/traits/method_concrete_unchanged.phpt b/Zend/tests/generics/traits/method_concrete_unchanged.phpt new file mode 100644 index 000000000000..ced7fe773e39 --- /dev/null +++ b/Zend/tests/generics/traits/method_concrete_unchanged.phpt @@ -0,0 +1,25 @@ +--TEST-- +Traits: concretely typed parameters and returns are unaffected by substitution +--FILE-- + { + public T $val; + public function takeT(T $x): T { return $x; } + public function bare(int $x): string { return (string)$x; } +} + +class StrHolder { use Holder; } + +$rm = (new ReflectionClass('StrHolder'))->getMethod('bare'); +echo "bare: ", + $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; + +$rm = (new ReflectionClass('StrHolder'))->getMethod('takeT'); +echo "takeT: ", + $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +bare: int -> string +takeT: string -> string diff --git a/Zend/tests/generics/traits/method_default_arg.phpt b/Zend/tests/generics/traits/method_default_arg.phpt new file mode 100644 index 000000000000..91eca8edefd1 --- /dev/null +++ b/Zend/tests/generics/traits/method_default_arg.phpt @@ -0,0 +1,20 @@ +--TEST-- +Traits: omitting use args fills the trait parameter defaults into method signatures +--FILE-- + { + public function f(T $x): T { return $x; } +} + +class Impl { use Defaulted; } + +$rm = (new ReflectionClass('Impl'))->getMethod('f'); +echo $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; + +$i = new Impl; +echo $i->f(42), "\n"; +?> +--EXPECT-- +int -> int +42 diff --git a/Zend/tests/generics/traits/method_signature_substituted.phpt b/Zend/tests/generics/traits/method_signature_substituted.phpt new file mode 100644 index 000000000000..578cf1d11976 --- /dev/null +++ b/Zend/tests/generics/traits/method_signature_substituted.phpt @@ -0,0 +1,25 @@ +--TEST-- +Traits: trait method param and return types are substituted from the use clause's bound arguments +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class IntHolder { use Holder; } +class StrHolder { use Holder; } + +foreach (['IntHolder', 'StrHolder'] as $cn) { + $rm = (new ReflectionClass($cn))->getMethod('take'); + $p = $rm->getParameters()[0]->getType()->getName(); + $r = $rm->getReturnType()->getName(); + echo "$cn::take: $p -> $r\n"; +} + +$ih = new IntHolder; +echo $ih->take(7), "\n"; +?> +--EXPECT-- +IntHolder::take: int -> int +StrHolder::take: string -> string +7 diff --git a/Zend/tests/generics/traits/method_variadic_substituted.phpt b/Zend/tests/generics/traits/method_variadic_substituted.phpt new file mode 100644 index 000000000000..cf0cb4c50ede --- /dev/null +++ b/Zend/tests/generics/traits/method_variadic_substituted.phpt @@ -0,0 +1,19 @@ +--TEST-- +Traits: variadic parameter typed T is substituted in the using class +--FILE-- + { + public function all(T ...$items): int { return count($items); } +} + +class IntAcceptor { use Acceptor; } + +$rm = (new ReflectionClass('IntAcceptor'))->getMethod('all'); +echo "param type: ", $rm->getParameters()[0]->getType()->getName(), "\n"; + +$a = new IntAcceptor; +echo $a->all(1, 2, 3), "\n"; +?> +--EXPECT-- +param type: int +3 diff --git a/Zend/tests/generics/traits/multiple_traits.phpt b/Zend/tests/generics/traits/multiple_traits.phpt new file mode 100644 index 000000000000..b21a673dcab5 --- /dev/null +++ b/Zend/tests/generics/traits/multiple_traits.phpt @@ -0,0 +1,22 @@ +--TEST-- +Traits: class uses multiple generic traits +--FILE-- + { + public function a(): X { return $this->ax; } + public X $ax; +} +trait B { + public function b(): Y { return $this->by; } + public Y $by; +} +class C { + use A, B; +} +$rc = new ReflectionClass('C'); +echo $rc->getMethod('a')->getReturnType()->getName(), "\n"; +echo $rc->getMethod('b')->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +object +int diff --git a/Zend/tests/generics/traits/property_no_substitution_when_concrete.phpt b/Zend/tests/generics/traits/property_no_substitution_when_concrete.phpt new file mode 100644 index 000000000000..8d93cc907242 --- /dev/null +++ b/Zend/tests/generics/traits/property_no_substitution_when_concrete.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: property already concretely typed in the trait is unaffected by `use` clause arguments +--FILE-- + { + public int $count = 0; + public T $val; +} + +class StrBox { use Holder; } + +$rcCount = (new ReflectionClass('StrBox'))->getProperty('count'); +$rcVal = (new ReflectionClass('StrBox'))->getProperty('val'); +echo $rcCount->getType()->getName(), "/", $rcVal->getType()->getName(), "\n"; +?> +--EXPECT-- +int/string diff --git a/Zend/tests/generics/traits/property_substituted_two_classes.phpt b/Zend/tests/generics/traits/property_substituted_two_classes.phpt new file mode 100644 index 000000000000..b0b3680f2d97 --- /dev/null +++ b/Zend/tests/generics/traits/property_substituted_two_classes.phpt @@ -0,0 +1,27 @@ +--TEST-- +Traits: two classes using the same generic trait with different bindings get distinct property types +--FILE-- + { + public T $val; +} + +class IntBox { use Holder; } +class StrBox { use Holder; } + +$ib = new IntBox; +$ib->val = 1; +$sb = new StrBox; +$sb->val = "x"; + +$rcInt = (new ReflectionClass('IntBox'))->getProperty('val'); +$rcStr = (new ReflectionClass('StrBox'))->getProperty('val'); +echo $rcInt->getType()->getName(), "/", $rcStr->getType()->getName(), "\n"; + +try { $ib->val = "no"; } catch (TypeError) { echo "TypeError IntBox\n"; } +try { $sb->val = []; } catch (TypeError) { echo "TypeError StrBox\n"; } +?> +--EXPECT-- +int/string +TypeError IntBox +TypeError StrBox diff --git a/Zend/tests/generics/traits/property_substituted_with_class_arg.phpt b/Zend/tests/generics/traits/property_substituted_with_class_arg.phpt new file mode 100644 index 000000000000..d2d8d39e370c --- /dev/null +++ b/Zend/tests/generics/traits/property_substituted_with_class_arg.phpt @@ -0,0 +1,29 @@ +--TEST-- +Traits: trait property substituted with a class type +--FILE-- + { + public T $val; +} + +class AnimalBox { use Holder; } + +$rp = (new ReflectionClass('AnimalBox'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; + +$b = new AnimalBox; +$b->val = new Animal; +echo get_class($b->val), "\n"; + +try { + $b->val = 5; +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +Animal +Animal +TypeError diff --git a/Zend/tests/generics/traits/trait_constant_type.phpt b/Zend/tests/generics/traits/trait_constant_type.phpt new file mode 100644 index 000000000000..860a0cd34c75 --- /dev/null +++ b/Zend/tests/generics/traits/trait_constant_type.phpt @@ -0,0 +1,18 @@ +--TEST-- +Traits: trait class constant with type parameter +--FILE-- + { + const T MIN = 0; +} +class Box { + use Holder; +} +$rc = new ReflectionClass('Box'); +$consts = $rc->getReflectionConstants(); +foreach ($consts as $c) { + echo $c->getName(), ": ", $c->getValue(), "\n"; +} +?> +--EXPECT-- +MIN: 0 diff --git a/Zend/tests/generics/traits/trait_get_generic_params.phpt b/Zend/tests/generics/traits/trait_get_generic_params.phpt new file mode 100644 index 000000000000..0a437a414821 --- /dev/null +++ b/Zend/tests/generics/traits/trait_get_generic_params.phpt @@ -0,0 +1,14 @@ +--TEST-- +Traits: ReflectionClass::getGenericParameters on trait +--FILE-- + {} +$rc = new ReflectionClass('T'); +foreach ($rc->getGenericParameters() as $p) { + echo $p->getName(), "\n"; +} +?> +--EXPECT-- +A +B +C diff --git a/Zend/tests/generics/traits/trait_isgeneric.phpt b/Zend/tests/generics/traits/trait_isgeneric.phpt new file mode 100644 index 000000000000..b142dfe56deb --- /dev/null +++ b/Zend/tests/generics/traits/trait_isgeneric.phpt @@ -0,0 +1,12 @@ +--TEST-- +Traits: ReflectionClass::isGeneric on trait +--FILE-- + {} +trait U {} +echo (new ReflectionClass('T'))->isGeneric() ? "T:gen\n" : "T:not\n"; +echo (new ReflectionClass('U'))->isGeneric() ? "U:gen\n" : "U:not\n"; +?> +--EXPECT-- +T:gen +U:not diff --git a/Zend/tests/generics/traits/trait_method_isgeneric.phpt b/Zend/tests/generics/traits/trait_method_isgeneric.phpt new file mode 100644 index 000000000000..959155d68cfc --- /dev/null +++ b/Zend/tests/generics/traits/trait_method_isgeneric.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: trait method's isGeneric and getGenericParameters +--FILE-- +(U $x): U { return $x; } +} +class C { use T; } +$rm = (new ReflectionClass('C'))->getMethod('f'); +echo $rm->isGeneric() ? "gen\n" : "not\n"; +foreach ($rm->getGenericParameters() as $p) { + echo $p->getName(), "\n"; +} +?> +--EXPECT-- +gen +U diff --git a/Zend/tests/generics/traits/trait_method_runtime.phpt b/Zend/tests/generics/traits/trait_method_runtime.phpt new file mode 100644 index 000000000000..324d473e45c9 --- /dev/null +++ b/Zend/tests/generics/traits/trait_method_runtime.phpt @@ -0,0 +1,26 @@ +--TEST-- +Traits: methods from trait work at runtime +--FILE-- + { + public function get(T $x): T { return $x; } +} +class Box { + use Holder; +} + +$a = new A; +$b = new Box; +echo get_class($b->get($a)), "\n"; + +try { + $b->get("string"); + echo "no error\n"; +} catch (TypeError $e) { + echo "type error\n"; +} +?> +--EXPECT-- +A +type error diff --git a/Zend/tests/generics/traits/trait_property_substituted.phpt b/Zend/tests/generics/traits/trait_property_substituted.phpt new file mode 100644 index 000000000000..a10364cf9450 --- /dev/null +++ b/Zend/tests/generics/traits/trait_property_substituted.phpt @@ -0,0 +1,32 @@ +--TEST-- +Traits: trait property type is substituted from the `use` clause's bound arguments +--FILE-- + { + public T $val; +} + +class IntBox { use Holder; } +class StringBox { use Holder; } + +$rp = (new ReflectionClass('IntBox'))->getProperty('val'); +echo "IntBox::\$val: ", $rp->getType()->getName(), "\n"; + +$rp = (new ReflectionClass('StringBox'))->getProperty('val'); +echo "StringBox::\$val: ", $rp->getType()->getName(), "\n"; + +$ib = new IntBox; +$ib->val = 7; +echo $ib->val, "\n"; + +try { + $ib->val = "x"; +} catch (TypeError $e) { + echo "TypeError\n"; +} +?> +--EXPECT-- +IntBox::$val: int +StringBox::$val: string +7 +TypeError diff --git a/Zend/tests/generics/traits/trait_property_type.phpt b/Zend/tests/generics/traits/trait_property_type.phpt new file mode 100644 index 000000000000..f6510a351ed3 --- /dev/null +++ b/Zend/tests/generics/traits/trait_property_type.phpt @@ -0,0 +1,16 @@ +--TEST-- +Traits: trait property type +--FILE-- + { + public T $val; +} +class Box { + use Holder; +} +$rp = (new ReflectionClass('Box'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; +?> +--EXPECT-- +Foo diff --git a/Zend/tests/generics/traits/trait_use_args_compile.phpt b/Zend/tests/generics/traits/trait_use_args_compile.phpt new file mode 100644 index 000000000000..84ae43ab0ff0 --- /dev/null +++ b/Zend/tests/generics/traits/trait_use_args_compile.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: trait use clause with type arguments compiles +--FILE-- + { + public function tag(X $x): X { return $x; } +} +class Box { + use Holder; +} +$b = new Box; +echo get_class($b), "\n"; +echo "ok\n"; +?> +--EXPECT-- +Box +ok diff --git a/Zend/tests/generics/traits/trait_with_method_param.phpt b/Zend/tests/generics/traits/trait_with_method_param.phpt new file mode 100644 index 000000000000..2203f9b20431 --- /dev/null +++ b/Zend/tests/generics/traits/trait_with_method_param.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: trait method declares its own type parameter +--FILE-- +(U $x): U { return $x; } +} +class C { + use T; +} +$rm = (new ReflectionClass('C'))->getMethod('f'); +echo $rm->getParameters()[0]->getType()->getName(), "\n"; +echo $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +int +int diff --git a/Zend/tests/generics/turbofish/arity/attribute_arity.phpt b/Zend/tests/generics/turbofish/arity/attribute_arity.phpt new file mode 100644 index 000000000000..9f6fa044538e --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/attribute_arity.phpt @@ -0,0 +1,38 @@ +--TEST-- +Turbofish arity: ReflectionAttribute::newInstance() enforces arity against the attribute class +--FILE-- + { + public function __construct(public string $tag) {} +} + +#[TaggedAttr::(tag: "ok")] +class A {} + +#[TaggedAttr::(tag: "bad")] +class B {} + +#[TaggedAttr(tag: "no-turbo")] +class D {} + +$ra = (new ReflectionClass("A"))->getAttributes()[0]; +echo "A: ", $ra->newInstance()->tag, "\n"; + +$rb = (new ReflectionClass("B"))->getAttributes()[0]; +try { + $rb->newInstance(); +} catch (ArgumentCountError $e) { + echo "B: ", $e->getMessage(), "\n"; +} + +// no turbofish at all -> no arity check, defaults apply (here T has no default) +// but newInstance() doesn't supply type args, which is arity 0; required is 1 -> still fails? No. +// arity 0 is treated as "no turbofish" and skips the check entirely. +$rd = (new ReflectionClass("D"))->getAttributes()[0]; +echo "D: ", $rd->newInstance()->tag, "\n"; +?> +--EXPECT-- +A: ok +B: Too many generic type arguments to new TaggedAttr, 2 passed and exactly 1 expected +D: no-turbo diff --git a/Zend/tests/generics/turbofish/arity/below_required.phpt b/Zend/tests/generics/turbofish/arity/below_required.phpt new file mode 100644 index 000000000000..09514d55a338 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/below_required.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish arity: providing fewer than required even with some defaults still throws +--FILE-- +($x) { return $x; } +try { + f::(1); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too few generic type arguments to f(), 1 passed and at least 2 expected diff --git a/Zend/tests/generics/turbofish/arity/function_too_few.phpt b/Zend/tests/generics/turbofish/arity/function_too_few.phpt new file mode 100644 index 000000000000..d5469444288d --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/function_too_few.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish arity: too few type arguments to a function throws ArgumentCountError +--FILE-- +($x, $y) { return [$x, $y]; } +try { + f::(1, 2); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too few generic type arguments to f(), 1 passed and exactly 2 expected diff --git a/Zend/tests/generics/turbofish/arity/function_too_many.phpt b/Zend/tests/generics/turbofish/arity/function_too_many.phpt new file mode 100644 index 000000000000..15747c2384fd --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/function_too_many.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish arity: too many type arguments to a function throws ArgumentCountError +--FILE-- +($x) { return $x; } +try { + f::(1); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too many generic type arguments to f(), 2 passed and exactly 1 expected diff --git a/Zend/tests/generics/turbofish/arity/method_arity.phpt b/Zend/tests/generics/turbofish/arity/method_arity.phpt new file mode 100644 index 000000000000..81eb1cb37219 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/method_arity.phpt @@ -0,0 +1,24 @@ +--TEST-- +Turbofish arity: instance method, static method, nullsafe call all enforce arity +--FILE-- +($x) { return $x; } + public static function s($x) { return $x; } +} + +$c = new C; + +try { $c->m::(1); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } + +try { $c?->m::(1); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } + +try { C::s::(1); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECT-- +Too many generic type arguments to C::m(), 2 passed and exactly 1 expected +Too many generic type arguments to C::m(), 2 passed and exactly 1 expected +Too few generic type arguments to C::s(), 1 passed and exactly 2 expected diff --git a/Zend/tests/generics/turbofish/arity/new_arity.phpt b/Zend/tests/generics/turbofish/arity/new_arity.phpt new file mode 100644 index 000000000000..de36227a7502 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/new_arity.phpt @@ -0,0 +1,28 @@ +--TEST-- +Turbofish arity: new ::<...> enforces arity against class generic parameters +--FILE-- + { + public function __construct(public int $v) {} +} +class Pair {} +class Plain {} + +try { new Box::(1); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } + +try { new Plain::(); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } + +// Pair: K required, V has default - so 1 or 2 args is OK, 0 or 3 is not +$p = new Pair::(); +echo get_class($p), "\n"; + +try { new Pair::(); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECT-- +Too many generic type arguments to new Box, 2 passed and exactly 1 expected +Too many generic type arguments to new Plain, 1 passed and exactly 0 expected +Pair +Too many generic type arguments to new Pair, 3 passed and at most 2 expected diff --git a/Zend/tests/generics/turbofish/arity/non_generic_callee.phpt b/Zend/tests/generics/turbofish/arity/non_generic_callee.phpt new file mode 100644 index 000000000000..5e7a6a622205 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/non_generic_callee.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish arity: turbofish on a non-generic callee throws ArgumentCountError +--FILE-- +(42); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too many generic type arguments to f(), 1 passed and exactly 0 expected diff --git a/Zend/tests/generics/turbofish/arity/with_defaults.phpt b/Zend/tests/generics/turbofish/arity/with_defaults.phpt new file mode 100644 index 000000000000..24509e622cb2 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/with_defaults.phpt @@ -0,0 +1,27 @@ +--TEST-- +Turbofish arity: defaults extend the accepted arity range +--FILE-- +($x) { return $x; } + +// arity 1, 2, 3 all valid +var_dump(f::(1)); +var_dump(f::(2)); +var_dump(f::(3)); + +// arity 0 (i.e., no turbofish) is also valid +var_dump(f(4)); + +// arity 4 exceeds total +try { + f::(5); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +int(1) +int(2) +int(3) +int(4) +Too many generic type arguments to f(), 4 passed and at most 3 expected diff --git a/Zend/tests/generics/turbofish/bounds/attribute_satisfies.phpt b/Zend/tests/generics/turbofish/bounds/attribute_satisfies.phpt new file mode 100644 index 000000000000..7d346132f2f6 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/attribute_satisfies.phpt @@ -0,0 +1,22 @@ +--TEST-- +Turbofish bound check: attribute newInstance accepts an arg that satisfies the bound +--FILE-- + { + public function __construct() {} +} + +#[Bounded::] +function f(): void {} + +foreach ((new ReflectionFunction('f'))->getAttributes() as $attr) { + $attr->newInstance(); +} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/turbofish/bounds/attribute_under_opcache.phpt b/Zend/tests/generics/turbofish/bounds/attribute_under_opcache.phpt new file mode 100644 index 000000000000..5cbc57dbd645 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/attribute_under_opcache.phpt @@ -0,0 +1,28 @@ +--TEST-- +Turbofish bound check: attribute generic args are persisted across opcache file cache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +--FILE-- + { + public function __construct() {} +} + +#[Bounded::] +function f(): void {} + +foreach ((new ReflectionFunction('f'))->getAttributes() as $attr) { + try { + $attr->newInstance(); + } catch (TypeError $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +Type argument 1 to new Bounded does not satisfy the bound Animal on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/attribute_violates.phpt b/Zend/tests/generics/turbofish/bounds/attribute_violates.phpt new file mode 100644 index 000000000000..c13cb4e0dd3a --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/attribute_violates.phpt @@ -0,0 +1,24 @@ +--TEST-- +Turbofish bound check: attribute newInstance rejects a type argument that violates the bound +--FILE-- + { + public function __construct() {} +} + +#[Bounded::] +function f(): void {} + +foreach ((new ReflectionFunction('f'))->getAttributes() as $attr) { + try { + $attr->newInstance(); + } catch (TypeError $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +Type argument 1 to new Bounded does not satisfy the bound Animal on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/error_renders_args.phpt b/Zend/tests/generics/turbofish/bounds/error_renders_args.phpt new file mode 100644 index 000000000000..76993541f1ea --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/error_renders_args.phpt @@ -0,0 +1,16 @@ +--TEST-- +Bound error message: NAMED_WITH_ARGS bound is rendered with its type arguments +--FILE-- + {} + +class Sorter> {} + +try { + new Sorter::; +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to new Sorter does not satisfy the bound Comparable on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/function_call_violates.phpt b/Zend/tests/generics/turbofish/bounds/function_call_violates.phpt new file mode 100644 index 000000000000..354e183508e4 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/function_call_violates.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish bound check: function call with a concrete arg violating the bound +--FILE-- +(): void {} + +try { + id::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to call id() does not satisfy the bound Animal on parameter T, string given diff --git a/Zend/tests/generics/turbofish/bounds/method_call_violates.phpt b/Zend/tests/generics/turbofish/bounds/method_call_violates.phpt new file mode 100644 index 000000000000..7abf3a3d42cf --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/method_call_violates.phpt @@ -0,0 +1,25 @@ +--TEST-- +Turbofish bound check: method call with a concrete arg violating the bound +--FILE-- +(): void {} + public static function spick(): void {} +} + +try { + (new Service)->pick::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} + +try { + Service::spick::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to call Service::pick() does not satisfy the bound Animal on parameter T, int given +Type argument 1 to call Service::spick() does not satisfy the bound Animal on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/multi_param_first_violates.phpt b/Zend/tests/generics/turbofish/bounds/multi_param_first_violates.phpt new file mode 100644 index 000000000000..e0729fe4c0d8 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/multi_param_first_violates.phpt @@ -0,0 +1,17 @@ +--TEST-- +Turbofish bound check: multi-parameter callee, first arg violates +--FILE-- +(): void {} + +try { + pair::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to call pair() does not satisfy the bound Animal on parameter A, int given diff --git a/Zend/tests/generics/turbofish/bounds/multi_param_second_violates.phpt b/Zend/tests/generics/turbofish/bounds/multi_param_second_violates.phpt new file mode 100644 index 000000000000..83bfbede7afa --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/multi_param_second_violates.phpt @@ -0,0 +1,17 @@ +--TEST-- +Turbofish bound check: multi-parameter callee, second arg violates +--FILE-- +(): void {} + +try { + pair::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 2 to call pair() does not satisfy the bound Dog on parameter B, Animal given diff --git a/Zend/tests/generics/turbofish/bounds/new_satisfies.phpt b/Zend/tests/generics/turbofish/bounds/new_satisfies.phpt new file mode 100644 index 000000000000..a1bb8d8339d1 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/new_satisfies.phpt @@ -0,0 +1,14 @@ +--TEST-- +Turbofish bound check: new with a concrete arg satisfying the bound +--FILE-- + {} + +new Box::; +new Box::; +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/turbofish/bounds/new_violates.phpt b/Zend/tests/generics/turbofish/bounds/new_violates.phpt new file mode 100644 index 000000000000..96734fb2c288 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/new_violates.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish bound check: new with a concrete arg violating the bound throws TypeError +--FILE-- + {} + +try { + new Box::; +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to new Box does not satisfy the bound Animal on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/no_bound_no_check.phpt b/Zend/tests/generics/turbofish/bounds/no_bound_no_check.phpt new file mode 100644 index 000000000000..f2f586086d2a --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/no_bound_no_check.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish bound check: parameters without bounds accept any type +--FILE-- + {} +function id(): void {} + +new Box::; +new Box::; +id::(); +id::(); +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/turbofish/composite_args.phpt b/Zend/tests/generics/turbofish/composite_args.phpt new file mode 100644 index 000000000000..8c69b6555b8b --- /dev/null +++ b/Zend/tests/generics/turbofish/composite_args.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish: union and intersection type arguments +--FILE-- +($x) { return $x; } +var_dump(f::(42)); +var_dump(f::(42)); +var_dump(f::<(A&B)|C>(42)); +?> +--EXPECT-- +int(42) +int(42) +int(42) diff --git a/Zend/tests/generics/turbofish/fcc_function.phpt b/Zend/tests/generics/turbofish/fcc_function.phpt new file mode 100644 index 000000000000..c304fb7ceed2 --- /dev/null +++ b/Zend/tests/generics/turbofish/fcc_function.phpt @@ -0,0 +1,10 @@ +--TEST-- +Turbofish: first-class callable on function +--FILE-- +($x) { return $x; } +$cl = id::(...); +var_dump($cl(7)); +?> +--EXPECT-- +int(7) diff --git a/Zend/tests/generics/turbofish/fcc_method.phpt b/Zend/tests/generics/turbofish/fcc_method.phpt new file mode 100644 index 000000000000..48436c4faee5 --- /dev/null +++ b/Zend/tests/generics/turbofish/fcc_method.phpt @@ -0,0 +1,10 @@ +--TEST-- +Turbofish: first-class callable on instance method +--FILE-- +($x) { return $x * 2; } } +$cl = (new C)->m::(...); +var_dump($cl(5)); +?> +--EXPECT-- +int(10) diff --git a/Zend/tests/generics/turbofish/fcc_static_method.phpt b/Zend/tests/generics/turbofish/fcc_static_method.phpt new file mode 100644 index 000000000000..56eaf1d671cd --- /dev/null +++ b/Zend/tests/generics/turbofish/fcc_static_method.phpt @@ -0,0 +1,10 @@ +--TEST-- +Turbofish: first-class callable on static method +--FILE-- +($x) { return $x; } } +$cl = C::m::(...); +var_dump($cl(11)); +?> +--EXPECT-- +int(11) diff --git a/Zend/tests/generics/turbofish/function_call.phpt b/Zend/tests/generics/turbofish/function_call.phpt new file mode 100644 index 000000000000..8de497d7136e --- /dev/null +++ b/Zend/tests/generics/turbofish/function_call.phpt @@ -0,0 +1,9 @@ +--TEST-- +Turbofish: function call with single type argument +--FILE-- +($x) { return $x; } +var_dump(f::(42)); +?> +--EXPECT-- +int(42) diff --git a/Zend/tests/generics/turbofish/function_call_multi.phpt b/Zend/tests/generics/turbofish/function_call_multi.phpt new file mode 100644 index 000000000000..f9f921f6751f --- /dev/null +++ b/Zend/tests/generics/turbofish/function_call_multi.phpt @@ -0,0 +1,14 @@ +--TEST-- +Turbofish: function call with multiple type arguments +--FILE-- +($a, $b) { return [$a, $b]; } +var_dump(combine::(1, "x")); +?> +--EXPECT-- +array(2) { + [0]=> + int(1) + [1]=> + string(1) "x" +} diff --git a/Zend/tests/generics/turbofish/in_chain.phpt b/Zend/tests/generics/turbofish/in_chain.phpt new file mode 100644 index 000000000000..f11b82a5c847 --- /dev/null +++ b/Zend/tests/generics/turbofish/in_chain.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish: chained calls +--FILE-- +() { return $this; } + public function n($x) { return $x; } +} +$c = new C; +var_dump($c->m::()->n::("hello")); +?> +--EXPECT-- +string(5) "hello" diff --git a/Zend/tests/generics/turbofish/instance_method_call.phpt b/Zend/tests/generics/turbofish/instance_method_call.phpt new file mode 100644 index 000000000000..a93855b4fb14 --- /dev/null +++ b/Zend/tests/generics/turbofish/instance_method_call.phpt @@ -0,0 +1,12 @@ +--TEST-- +Turbofish: instance method call +--FILE-- +($x) { return $x; } +} +$c = new C; +var_dump($c->m::(99)); +?> +--EXPECT-- +int(99) diff --git a/Zend/tests/generics/turbofish/instanceof_with_args.phpt b/Zend/tests/generics/turbofish/instanceof_with_args.phpt new file mode 100644 index 000000000000..3da4129537b5 --- /dev/null +++ b/Zend/tests/generics/turbofish/instanceof_with_args.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish: instanceof discards type arguments +--FILE-- + {} +class C {} +$c = new C; +var_dump($c instanceof C); +var_dump($c instanceof C); +var_dump($c instanceof C>); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/turbofish/many_type_args.phpt b/Zend/tests/generics/turbofish/many_type_args.phpt new file mode 100644 index 000000000000..59d5193f5e1a --- /dev/null +++ b/Zend/tests/generics/turbofish/many_type_args.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish: arity must match callee's declared generic parameters +--FILE-- +($x) { return $x; } +try { + f::(7); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too many generic type arguments to f(), 3 passed and exactly 1 expected diff --git a/Zend/tests/generics/turbofish/nested_args.phpt b/Zend/tests/generics/turbofish/nested_args.phpt new file mode 100644 index 000000000000..ec9f4c25fcbf --- /dev/null +++ b/Zend/tests/generics/turbofish/nested_args.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish: nested type arguments +--FILE-- + {} +function f($x) { return $x; } +var_dump(f::>(["a", "b"])); +?> +--EXPECT-- +array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" +} diff --git a/Zend/tests/generics/turbofish/new_with_args.phpt b/Zend/tests/generics/turbofish/new_with_args.phpt new file mode 100644 index 000000000000..494aeb67dbe1 --- /dev/null +++ b/Zend/tests/generics/turbofish/new_with_args.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish: new with type arguments +--FILE-- + { + public int $x; + public function __construct(int $x) { $this->x = $x; } +} +$b = new Box::(42); +echo $b->x, "\n"; +?> +--EXPECT-- +42 diff --git a/Zend/tests/generics/turbofish/no_runtime_effect.phpt b/Zend/tests/generics/turbofish/no_runtime_effect.phpt new file mode 100644 index 000000000000..632fee88d045 --- /dev/null +++ b/Zend/tests/generics/turbofish/no_runtime_effect.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish: with matching arity has zero runtime effect on values +--FILE-- +(int $x): int { return $x * 2; } +var_dump(f(5)); +var_dump(f::(5)); +var_dump(f::(5)); +var_dump(f::(5)); +?> +--EXPECT-- +int(10) +int(10) +int(10) +int(10) diff --git a/Zend/tests/generics/turbofish/nullsafe_method_call.phpt b/Zend/tests/generics/turbofish/nullsafe_method_call.phpt new file mode 100644 index 000000000000..fe4d04782ecc --- /dev/null +++ b/Zend/tests/generics/turbofish/nullsafe_method_call.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish: nullsafe method call +--FILE-- +($x) { return $x; } +} +$c = new C; +var_dump($c?->m::(5)); +$null = null; +var_dump($null?->m::(5)); +?> +--EXPECT-- +int(5) +NULL diff --git a/Zend/tests/generics/turbofish/static_method_call.phpt b/Zend/tests/generics/turbofish/static_method_call.phpt new file mode 100644 index 000000000000..681b35adac81 --- /dev/null +++ b/Zend/tests/generics/turbofish/static_method_call.phpt @@ -0,0 +1,11 @@ +--TEST-- +Turbofish: static method call +--FILE-- +($x) { return $x; } +} +var_dump(C::m::(7)); +?> +--EXPECT-- +int(7) diff --git a/Zend/tests/generics/turbofish/turbofish_in_attribute.phpt b/Zend/tests/generics/turbofish/turbofish_in_attribute.phpt new file mode 100644 index 000000000000..c8822c08ccf7 --- /dev/null +++ b/Zend/tests/generics/turbofish/turbofish_in_attribute.phpt @@ -0,0 +1,39 @@ +--TEST-- +Turbofish: type arguments are accepted on attribute declarations +--FILE-- + { + public function __construct(public int $priority = 0) {} +} + +class UserCreated {} +class UserDeleted {} +class OrderPlaced {} + +class EventListener { + #[Listens::] + public function onUserCreated(object $event): void {} + + #[Listens::()] + public function onUserDeleted(object $event): void {} + + #[Listens::(priority: -1)] + public function onOrderPlaced(object $event): void {} +} + +$rc = new ReflectionClass(EventListener::class); +foreach ($rc->getMethods() as $method) { + foreach ($method->getAttributes() as $attr) { + $instance = $attr->newInstance(); + echo $method->getName(), ": #[", $attr->getName(), + "] priority=", $instance->priority, "\n"; + } +} +?> +--EXPECT-- +onUserCreated: #[Listens] priority=0 +onUserDeleted: #[Listens] priority=0 +onOrderPlaced: #[Listens] priority=-1 diff --git a/Zend/tests/generics/turbofish/with_named_args.phpt b/Zend/tests/generics/turbofish/with_named_args.phpt new file mode 100644 index 000000000000..3514c99fb2ed --- /dev/null +++ b/Zend/tests/generics/turbofish/with_named_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Turbofish: combined with named arguments +--FILE-- +($a, $b) { return "$a:$b"; } +echo f::(a: 1, b: 2), "\n"; +echo f::(b: "x", a: 5), "\n"; +?> +--EXPECT-- +1:2 +5:x diff --git a/Zend/tests/generics/turbofish/with_spread.phpt b/Zend/tests/generics/turbofish/with_spread.phpt new file mode 100644 index 000000000000..c35332c212a6 --- /dev/null +++ b/Zend/tests/generics/turbofish/with_spread.phpt @@ -0,0 +1,10 @@ +--TEST-- +Turbofish: combined with spread/unpacking +--FILE-- +($a, $b, $c) { return $a + $b + $c; } +$args = [1, 2, 3]; +echo f::(...$args), "\n"; +?> +--EXPECT-- +6 diff --git a/Zend/zend.h b/Zend/zend.h index 0d5303192b57..dcaf46ad462f 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -228,6 +228,9 @@ struct _zend_class_entry { zend_string *doc_comment; + struct _zend_generic_parameter_list *generic_parameters; + struct _zend_generic_type_table *generic_types; + union { struct { zend_string *filename; diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index f495c4c8e3bb..e599fe1d7b8e 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -136,7 +136,8 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_class_const_or_name(zend_ast * ZEND_API zend_ast *zend_ast_create_decl( zend_ast_kind kind, uint32_t flags, uint32_t start_lineno, zend_string *doc_comment, - zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4 + zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, + zend_ast *child4, zend_ast *child5 ) { zend_ast_decl *ast; @@ -153,6 +154,7 @@ ZEND_API zend_ast *zend_ast_create_decl( ast->child[2] = child2; ast->child[3] = child3; ast->child[4] = child4; + ast->child[5] = child5; return (zend_ast *) ast; } @@ -1504,7 +1506,8 @@ ZEND_API void ZEND_FASTCALL zend_ast_destroy(zend_ast *ast) zend_ast_destroy(decl->child[1]); zend_ast_destroy(decl->child[2]); zend_ast_destroy(decl->child[3]); - ast = decl->child[4]; + zend_ast_destroy(decl->child[4]); + ast = decl->child[5]; goto tail_call; } else if (EXPECTED(ast->kind == ZEND_AST_CALLABLE_CONVERT)) { zend_ast_fcc *fcc_ast = (zend_ast_fcc*) ast; @@ -2028,6 +2031,8 @@ static ZEND_COLD void zend_ast_export_visibility(smart_str *str, uint32_t flags, } } +static ZEND_COLD void zend_ast_export_generic_type_argument_list(smart_str *str, zend_ast *list_ast, int indent); + static ZEND_COLD void zend_ast_export_type(smart_str *str, zend_ast *ast, int indent) { if (ast->kind == ZEND_AST_TYPE_UNION) { const zend_ast_list *list = zend_ast_get_list(ast); @@ -2049,12 +2054,88 @@ static ZEND_COLD void zend_ast_export_type(smart_str *str, zend_ast *ast, int in } return; } + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + if (ast->attr & ZEND_TYPE_NULLABLE) { + smart_str_appendc(str, '?'); + } + + zend_ast *name_ast = ast->child[0]; + if (name_ast->kind == ZEND_AST_TYPE) { + zend_ast_export_type(str, name_ast, indent); + } else { + zend_ast_export_ns_name(str, name_ast, 0, indent); + } + zend_ast_export_generic_type_argument_list(str, ast->child[1], indent); + return; + } if (ast->attr & ZEND_TYPE_NULLABLE) { smart_str_appendc(str, '?'); } zend_ast_export_ns_name(str, ast, 0, indent); } +static ZEND_COLD void zend_ast_export_generic_type_argument_list(smart_str *str, zend_ast *list_ast, int indent) +{ + const zend_ast_list *list = zend_ast_get_list(list_ast); + smart_str_appendc(str, '<'); + for (uint32_t i = 0; i < list->children; i++) { + if (i != 0) { + smart_str_appends(str, ", "); + } + zend_ast_export_type(str, list->child[i], indent); + } + smart_str_appendc(str, '>'); +} + +static ZEND_COLD void zend_ast_export_generic_call_type_argument_list(smart_str *str, zend_ast *list_ast, int indent) +{ + const zend_ast_list *list = zend_ast_get_list(list_ast); + smart_str_appends(str, "::<"); + for (uint32_t i = 0; i < list->children; i++) { + if (i != 0) { + smart_str_appends(str, ", "); + } + zend_ast_export_type(str, list->child[i], indent); + } + smart_str_appendc(str, '>'); +} + +static ZEND_COLD void zend_ast_export_generic_type_parameter(smart_str *str, zend_ast *ast, int indent) +{ + switch ((zend_generic_variance) ast->attr) { + case ZEND_GENERIC_VARIANCE_COVARIANT: + smart_str_appendc(str, '+'); + break; + case ZEND_GENERIC_VARIANCE_CONTRAVARIANT: + smart_str_appendc(str, '-'); + break; + case ZEND_GENERIC_VARIANCE_INVARIANT: + break; + } + smart_str_append(str, zend_ast_get_str(ast->child[0])); + if (ast->child[1]) { + smart_str_appends(str, " : "); + zend_ast_export_type(str, ast->child[1], indent); + } + if (ast->child[2]) { + smart_str_appends(str, " = "); + zend_ast_export_type(str, ast->child[2], indent); + } +} + +static ZEND_COLD void zend_ast_export_generic_type_parameter_list(smart_str *str, zend_ast *list_ast, int indent) +{ + const zend_ast_list *list = zend_ast_get_list(list_ast); + smart_str_appendc(str, '<'); + for (uint32_t i = 0; i < list->children; i++) { + if (i != 0) { + smart_str_appends(str, ", "); + } + zend_ast_export_generic_type_parameter(str, list->child[i], indent); + } + smart_str_appendc(str, '>'); +} + static ZEND_COLD void zend_ast_export_hook_list(smart_str *str, const zend_ast_list *hook_list, int indent) { smart_str_appends(str, " {"); @@ -2204,6 +2285,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio if (ast->kind != ZEND_AST_CLOSURE && ast->kind != ZEND_AST_ARROW_FUNC) { smart_str_append(str, decl->name); } + if (decl->child[5]) { + zend_ast_export_generic_type_parameter_list(str, decl->child[5], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, decl->child[0], 0, indent); smart_str_appendc(str, ')'); @@ -2261,6 +2345,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio smart_str_appends(str, "class "); } smart_str_append(str, decl->name); + if (decl->child[5]) { + zend_ast_export_generic_type_parameter_list(str, decl->child[5], indent); + } if (decl->flags & ZEND_ACC_ENUM && decl->child[4]) { smart_str_appends(str, ": "); zend_ast_export_type(str, decl->child[4], indent); @@ -2367,6 +2454,15 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio case ZEND_AST_NAME_LIST: zend_ast_export_name_list(str, zend_ast_get_list(ast), indent); break; + case ZEND_AST_GENERIC_TYPE_PARAMETER_LIST: + zend_ast_export_generic_type_parameter_list(str, ast, indent); + break; + case ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST: + zend_ast_export_generic_type_argument_list(str, ast, indent); + break; + case ZEND_AST_GENERIC_CALL_TYPE_ARGUMENT_LIST: + zend_ast_export_generic_call_type_argument_list(str, ast, indent); + break; case ZEND_AST_USE: smart_str_appends(str, "use "); if (ast->attr == T_FUNCTION) { @@ -2542,6 +2638,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio } else { zend_ast_export_ns_name(str, left, 0, indent); } + if (ast->child[2]) { + zend_ast_export_generic_call_type_argument_list(str, ast->child[2], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[1], 0, indent); smart_str_appendc(str, ')'); @@ -2650,6 +2749,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_class_no_header(str, decl, indent); } else { zend_ast_export_ns_name(str, ast->child[0], 0, indent); + if (ast->child[2]) { + zend_ast_export_generic_call_type_argument_list(str, ast->child[2], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[1], 0, indent); smart_str_appendc(str, ')'); @@ -2660,6 +2762,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio smart_str_appends(str, " instanceof "); zend_ast_export_ns_name(str, ast->child[1], 0, indent); break; + case ZEND_AST_GENERIC_NAMED_TYPE: + zend_ast_export_type(str, ast, indent); + break; case ZEND_AST_YIELD: if (priority > 70) smart_str_appendc(str, '('); smart_str_appends(str, "yield "); @@ -2840,12 +2945,15 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio ast = ast->child[1]; goto tail_call; - /* 3 child nodes */ + /* 4 child nodes */ case ZEND_AST_METHOD_CALL: case ZEND_AST_NULLSAFE_METHOD_CALL: zend_ast_export_ex(str, ast->child[0], 0, indent); smart_str_appends(str, ast->kind == ZEND_AST_NULLSAFE_METHOD_CALL ? "?->" : "->"); zend_ast_export_var(str, ast->child[1], indent); + if (ast->child[3]) { + zend_ast_export_generic_call_type_argument_list(str, ast->child[3], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[2], 0, indent); smart_str_appendc(str, ')'); @@ -2854,6 +2962,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_ns_name(str, ast->child[0], 0, indent); smart_str_appends(str, "::"); zend_ast_export_var(str, ast->child[1], indent); + if (ast->child[3]) { + zend_ast_export_generic_call_type_argument_list(str, ast->child[3], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[2], 0, indent); smart_str_appendc(str, ')'); @@ -2934,6 +3045,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_ex(str, ast->child[1], 0, indent); } break; + case ZEND_AST_GENERIC_TYPE_PARAMETER: + zend_ast_export_generic_type_parameter(str, ast, indent); + break; /* 4 child nodes */ case ZEND_AST_FOR: diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 24b77d7d3493..77598f809f1c 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -69,6 +69,9 @@ enum _zend_ast_kind { ZEND_AST_ATTRIBUTE_GROUP, ZEND_AST_MATCH_ARM_LIST, ZEND_AST_MODIFIER_LIST, + ZEND_AST_GENERIC_TYPE_PARAMETER_LIST, + ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST, + ZEND_AST_GENERIC_CALL_TYPE_ARGUMENT_LIST, /* 0 child nodes */ ZEND_AST_MAGIC_CONST = 0 << ZEND_AST_NUM_CHILDREN_SHIFT, @@ -117,7 +120,6 @@ enum _zend_ast_kind { ZEND_AST_PROP, ZEND_AST_NULLSAFE_PROP, ZEND_AST_STATIC_PROP, - ZEND_AST_CALL, ZEND_AST_CLASS_CONST, ZEND_AST_ASSIGN, ZEND_AST_ASSIGN_REF, @@ -128,7 +130,6 @@ enum _zend_ast_kind { ZEND_AST_AND, ZEND_AST_OR, ZEND_AST_ARRAY_ELEM, - ZEND_AST_NEW, ZEND_AST_INSTANCEOF, ZEND_AST_YIELD, ZEND_AST_COALESCE, @@ -148,23 +149,23 @@ enum _zend_ast_kind { ZEND_AST_USE_ELEM, ZEND_AST_TRAIT_ALIAS, ZEND_AST_GROUP_USE, - ZEND_AST_ATTRIBUTE, ZEND_AST_MATCH, ZEND_AST_MATCH_ARM, ZEND_AST_NAMED_ARG, ZEND_AST_PIPE, + ZEND_AST_GENERIC_NAMED_TYPE, /* 3 child nodes */ - ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT, - ZEND_AST_NULLSAFE_METHOD_CALL, - ZEND_AST_STATIC_CALL, - ZEND_AST_CONDITIONAL, - + ZEND_AST_CONDITIONAL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT, ZEND_AST_TRY, ZEND_AST_CATCH, ZEND_AST_PROP_GROUP, ZEND_AST_CONST_ELEM, ZEND_AST_CLASS_CONST_GROUP, + ZEND_AST_CALL, + ZEND_AST_NEW, + ZEND_AST_GENERIC_TYPE_PARAMETER, + ZEND_AST_ATTRIBUTE, /* 4 child nodes */ ZEND_AST_FOR = 4 << ZEND_AST_NUM_CHILDREN_SHIFT, @@ -175,6 +176,11 @@ enum _zend_ast_kind { // Pseudo node for initializing enums ZEND_AST_CONST_ENUM_INIT, + /* Method/static call forms with optional call_type_arguments slot. */ + ZEND_AST_METHOD_CALL, + ZEND_AST_NULLSAFE_METHOD_CALL, + ZEND_AST_STATIC_CALL, + /* 5 child nodes */ /* 6 child nodes */ @@ -225,7 +231,7 @@ typedef struct _zend_ast_decl { uint32_t flags; zend_string *doc_comment; zend_string *name; - zend_ast *child[5]; + zend_ast *child[6]; } zend_ast_decl; // TODO: rename @@ -337,7 +343,8 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_arg_list_add(zend_ast *list, zend_ast ZEND_API zend_ast *zend_ast_create_decl( zend_ast_kind kind, uint32_t flags, uint32_t start_lineno, zend_string *doc_comment, - zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4 + zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, + zend_ast *child4, zend_ast *child5 ); ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_fcc(zend_ast *args); diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index 71f0d788e921..416be8093ef2 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -472,6 +472,11 @@ static void attr_free(zval *v) zend_string_release(attr->validation_error); } + if (attr->generic_args) { + zend_type_release(*attr->generic_args, persistent); + pefree(attr->generic_args, persistent); + } + for (uint32_t i = 0; i < attr->argc; i++) { if (attr->args[i].name) { zend_string_release(attr->args[i].name); @@ -508,6 +513,8 @@ ZEND_API zend_attribute *zend_add_attribute(HashTable **attributes, zend_string attr->lineno = lineno; attr->offset = offset; attr->argc = argc; + attr->generic_arity = 0; + attr->generic_args = NULL; /* Initialize arguments to avoid partial initialization in case of fatal errors. */ for (uint32_t i = 0; i < argc; i++) { diff --git a/Zend/zend_attributes.h b/Zend/zend_attributes.h index c044ef073cc8..628b557a6584 100644 --- a/Zend/zend_attributes.h +++ b/Zend/zend_attributes.h @@ -67,6 +67,9 @@ typedef struct _zend_attribute { /* Parameter offsets start at 1, everything else uses 0. */ uint32_t offset; uint32_t argc; + /* Number of generic type arguments at the attribute use-site. */ + uint8_t generic_arity; + zend_type *generic_args; zend_attribute_arg args[1]; } zend_attribute; diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 7e9f7ceac8db..9a27d3639277 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -22,6 +22,7 @@ #include "zend_ast.h" #include "zend_attributes.h" #include "zend_compile.h" +#include "zend_smart_str.h" #include "zend_constants.h" #include "zend_llist.h" #include "zend_API.h" @@ -101,6 +102,7 @@ static zend_op *zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t static void zend_compile_expr(znode *result, zend_ast *ast); static void zend_compile_stmt(zend_ast *ast); static void zend_compile_assign(znode *result, zend_ast *ast, bool stmt, uint32_t type); +static zend_type zend_compile_typename(zend_ast *ast); #ifdef ZEND_CHECK_STACK_LIMIT zend_never_inline static void zend_stack_limit_error(void) @@ -431,6 +433,644 @@ void zend_init_compiler_data_structures(void) /* {{{ */ CG(encoding_declared) = 0; CG(memoized_exprs) = NULL; CG(memoize_mode) = ZEND_MEMOIZE_NONE; + CG(type_arg_depth) = 0; + CG(type_arg_residual_token) = 0; + CG(generic_scope) = NULL; + CG(in_static_member_type) = false; + CG(inheritance_binding_cache) = NULL; +} +/* }}} */ + +static void zend_generic_scope_push(zend_generic_parameter_list *params, zend_generic_origin origin) /* {{{ */ +{ + zend_generic_scope_entry *entry = emalloc(sizeof(zend_generic_scope_entry)); + entry->params = params; + entry->visible_count = params ? params->count : 0; + entry->self_compiling = NULL; + entry->shadowing_classes = NULL; + entry->origin = origin; + entry->outer = CG(generic_scope); + CG(generic_scope) = entry; +} +/* }}} */ + +static void zend_generic_scope_pop(void) /* {{{ */ +{ + zend_generic_scope_entry *top = CG(generic_scope); + ZEND_ASSERT(top != NULL); + if (top->shadowing_classes) { + zend_hash_destroy(top->shadowing_classes); + FREE_HASHTABLE(top->shadowing_classes); + } + + CG(generic_scope) = top->outer; + efree(top); +} +/* }}} */ + +static zend_generic_parameter *zend_generic_lookup_full( + zend_string *name, zend_generic_origin *origin_out, uint32_t *index_out) /* {{{ */ +{ + zend_string *lc_name = NULL; + zend_generic_parameter *result = NULL; + for (zend_generic_scope_entry *e = CG(generic_scope); e; e = e->outer) { + if (e->shadowing_classes) { + if (!lc_name) { + lc_name = zend_string_tolower(name); + } + + if (zend_hash_exists(e->shadowing_classes, lc_name)) { + goto done; + } + } + + zend_generic_parameter_list *params = e->params; + for (uint32_t i = 0; i < e->visible_count; i++) { + if (zend_string_equals(params->parameters[i].name, name)) { + if (origin_out) *origin_out = e->origin; + if (index_out) *index_out = i; + result = ¶ms->parameters[i]; + goto done; + } + } + } + + done: + if (lc_name) { + zend_string_release(lc_name); + } + + return result; +} +/* }}} */ + +static void zend_check_class_origin_in_static_context(const zend_string *param_name) /* {{{ */ +{ + if (CG(in_static_member_type) && CG(active_class_entry)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s is bound to %s and cannot be used in static context", + ZSTR_VAL(param_name), ZSTR_VAL(CG(active_class_entry)->name)); + } +} +/* }}} */ + +/* Detects forward reference: a name that matches a parameter declared later in + * the innermost scope's parameter list. Returns the index of the not-yet-visible + * parameter, or -1 if no match. */ +static int zend_generic_lookup_forward(const zend_string *name) /* {{{ */ +{ + zend_generic_scope_entry *e = CG(generic_scope); + if (!e) { + return -1; + } + zend_generic_parameter_list *params = e->params; + for (uint32_t i = e->visible_count; i < params->count; i++) { + if (zend_string_equals(params->parameters[i].name, name)) { + return (int) i; + } + } + return -1; +} +/* }}} */ + +static bool zend_type_ast_has_generic_content(zend_ast *ast); +static zend_type zend_compile_pre_erasure_typename(zend_ast *ast); +static zend_string *zend_resolve_class_name(zend_string *name, uint32_t type); + +static zend_generic_parameter *zend_generic_lookup(zend_string *name) /* {{{ */ +{ + return zend_generic_lookup_full(name, NULL, NULL); +} +/* }}} */ + +ZEND_API void zend_check_generic_param_list_size(zend_ast *list_ast) /* {{{ */ +{ + if (list_ast == NULL) { + return; + } + + uint32_t children = zend_ast_get_list(list_ast)->children; + if (children > ZEND_GENERIC_MAX_PARAMS) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot declare more than %u generic type parameters (got %u)", + ZEND_GENERIC_MAX_PARAMS, children); + } +} +/* }}} */ + +ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast) /* {{{ */ +{ + if (list_ast == NULL) { + return; + } + + uint32_t children = zend_ast_get_list(list_ast)->children; + if (children > ZEND_GENERIC_MAX_PARAMS) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot specify more than %u generic type arguments (got %u)", + ZEND_GENERIC_MAX_PARAMS, children); + } +} +/* }}} */ + +static zend_always_inline void zend_compute_generic_required_total( + const zend_generic_parameter_list *params, uint32_t *required, uint32_t *total) +{ + uint32_t t = params ? params->count : 0; + uint32_t r = 0; + if (params) { + while (r < t && !ZEND_TYPE_IS_SET(params->parameters[r].default_type)) { + r++; + } + } + + *required = r; + *total = t; +} + +static zend_generic_type_table *zend_generic_get_or_create_op_array_table(zend_op_array *op_array); + +ZEND_API const zend_type *zend_generic_get_turbofish_args(const zend_op_array *caller_op_array, uint32_t args_id) +{ + if (!caller_op_array || !caller_op_array->generic_types || !caller_op_array->generic_types->turbofish_args || args_id == 0) { + return NULL; + } + + return zend_hash_index_find_ptr(caller_op_array->generic_types->turbofish_args, args_id); +} + +static void zend_check_generic_argument_bounds( + const zend_generic_parameter_list *params, + const zend_type *args_box, + uint32_t arity, + const char *callee_kind, + const char *callee_qualified_name) +{ + if (!params || !args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return; + } + + const zend_type_named_with_args *args = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + uint32_t check_count = arity < args->count ? arity : args->count; + if (check_count > params->count) check_count = params->count; + for (uint32_t i = 0; i < check_count; i++) { + zend_type bound = params->parameters[i].bound; + if (!ZEND_TYPE_IS_SET(bound)) { + continue; + } + if (zend_check_generic_arg_satisfies_bound(NULL, args->args[i], NULL, bound) != INHERITANCE_SUCCESS) { + zend_string *arg_str = zend_type_to_string(args->args[i]); + zend_type display = ZEND_TYPE_IS_SET(params->parameters[i].bound_pre_erasure) + ? params->parameters[i].bound_pre_erasure + : bound; + zend_string *bound_str = zend_type_to_string(display); + zend_throw_error(zend_ce_type_error, + "Type argument %u to %s %s does not satisfy the bound %s on parameter %s, %s given", + i + 1, callee_kind, callee_qualified_name, + ZSTR_VAL(bound_str), + params->parameters[i].name ? ZSTR_VAL(params->parameters[i].name) : "?", + ZSTR_VAL(arg_str)); + zend_string_release(arg_str); + zend_string_release(bound_str); + return; + } + } +} + +ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box) +{ + const zend_generic_parameter_list *params = NULL; + if (ZEND_USER_CODE(fbc->common.type)) { + params = fbc->op_array.generic_parameters; + } + + uint32_t required, total; + zend_compute_generic_required_total(params, &required, &total); + + if (arity > total) { + zend_throw_error(zend_ce_argument_count_error, + "Too many generic type arguments to %s%s%s(), %u passed and %s %u expected", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + arity, + required == total ? "exactly" : "at most", + total); + return; + } + if (arity < required) { + zend_throw_error(zend_ce_argument_count_error, + "Too few generic type arguments to %s%s%s(), %u passed and %s %u expected", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + arity, + required == total ? "exactly" : "at least", + required); + return; + } + + if (args_box) { + zend_string *qname = zend_strpprintf(0, "%s%s%s()", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}"); + zend_check_generic_argument_bounds(params, args_box, arity, "call", ZSTR_VAL(qname)); + zend_string_release(qname); + } +} + +ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint32_t arity, const zend_type *args_box) +{ + uint32_t required, total; + zend_compute_generic_required_total(ce->generic_parameters, &required, &total); + + if (arity > total) { + zend_throw_error(zend_ce_argument_count_error, + "Too many generic type arguments to new %s, %u passed and %s %u expected", + ZSTR_VAL(ce->name), arity, + required == total ? "exactly" : "at most", total); + return; + } + if (arity < required) { + zend_throw_error(zend_ce_argument_count_error, + "Too few generic type arguments to new %s, %u passed and %s %u expected", + ZSTR_VAL(ce->name), arity, + required == total ? "exactly" : "at least", required); + return; + } + + if (args_box) { + zend_check_generic_argument_bounds(ce->generic_parameters, args_box, arity, "new", ZSTR_VAL(ce->name)); + } +} + +#define ZEND_VERIFY_ARITY_KIND_CALL 0 +#define ZEND_VERIFY_ARITY_KIND_NEW 1 + +static zend_type zend_compile_turbofish_args_type(zend_ast *turbofish_ast) +{ + zend_ast_list *list = zend_ast_get_list(turbofish_ast); + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(list->children)); + payload->name = NULL; + payload->name_attr = 0; + payload->count = list->children; + for (uint32_t i = 0; i < list->children; i++) { + payload->args[i] = zend_compile_pre_erasure_typename(list->child[i]); + } + zend_type result = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, payload); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + return result; +} + +static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result) +{ + if (turbofish_ast == NULL) { + return; + } + uint32_t arity = zend_ast_get_list(turbofish_ast)->children; + ZEND_ASSERT(arity > 0 && arity <= ZEND_GENERIC_MAX_PARAMS); + + zend_type args_type = zend_compile_turbofish_args_type(turbofish_ast); + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + uint32_t args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, args_type); + + zend_op *opline = get_next_op(); + opline->opcode = ZEND_VERIFY_GENERIC_ARGUMENTS; + opline->op2_type = IS_UNUSED; + opline->op2.num = arity; + opline->extended_value = args_id; + if (kind == ZEND_VERIFY_ARITY_KIND_NEW) { + ZEND_ASSERT(new_result != NULL && new_result->op_type == IS_TMP_VAR); + opline->op1_type = new_result->op_type; + opline->op1.var = new_result->u.op.var; + } else { + opline->op1_type = IS_UNUSED; + opline->op1.num = 0; + } + opline->result_type = IS_UNUSED; + opline->result.num = 0; +} + +static zend_generic_parameter_list *zend_compile_generic_type_parameter_list( + zend_ast *list_ast, zend_generic_origin origin) /* {{{ */ +{ + if (!list_ast) { + return NULL; + } + zend_ast_list *list = zend_ast_get_list(list_ast); + ZEND_ASSERT(list->children > 0); + + zend_generic_parameter_list *params = + zend_generic_parameter_list_alloc(list->children, /* persistent */ false); + + zend_string *prev_optional_name = NULL; + + for (uint32_t i = 0; i < list->children; i++) { + zend_ast *param_ast = list->child[i]; + ZEND_ASSERT(param_ast->kind == ZEND_AST_GENERIC_TYPE_PARAMETER); + zend_string *name = zval_make_interned_string(zend_ast_get_zval(param_ast->child[0])); + bool has_default = param_ast->child[2] != NULL; + + for (uint32_t j = 0; j < i; j++) { + if (zend_string_equals(params->parameters[j].name, name)) { + zend_string *dup = zend_string_copy(name); + zend_generic_parameter_list_destroy(params); + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot redeclare type parameter %s", ZSTR_VAL(dup)); + } + } + + if (zend_generic_lookup(name)) { + zend_string *dup = zend_string_copy(name); + zend_generic_parameter_list_destroy(params); + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s shadows enclosing type parameter", ZSTR_VAL(dup)); + } + + if (!has_default && prev_optional_name != NULL) { + zend_string *cur = zend_string_copy(name); + zend_string *prev = zend_string_copy(prev_optional_name); + zend_generic_parameter_list_destroy(params); + zend_error_noreturn(E_COMPILE_ERROR, + "Optional type parameter %s cannot be declared before required type parameter %s", + ZSTR_VAL(prev), ZSTR_VAL(cur)); + } + + if (has_default && prev_optional_name == NULL) { + prev_optional_name = name; + } + + params->parameters[i].name = zend_string_copy(name); + params->parameters[i].variance = (zend_generic_variance) param_ast->attr; + } + + params->count = list->children; + + zend_generic_scope_push(params, origin); + + CG(generic_scope)->visible_count = list->children; + for (uint32_t i = 0; i < list->children; i++) { + zend_ast *param_ast = list->child[i]; + if (!param_ast->child[1]) { + continue; + } + + CG(generic_scope)->self_compiling = ¶ms->parameters[i]; + params->parameters[i].bound = zend_compile_typename(param_ast->child[1]); + if (zend_type_ast_has_generic_content(param_ast->child[1])) { + params->parameters[i].bound_pre_erasure = zend_compile_pre_erasure_typename(param_ast->child[1]); + } + + CG(generic_scope)->self_compiling = NULL; + } + + CG(generic_scope)->visible_count = 0; + for (uint32_t i = 0; i < list->children; i++) { + zend_ast *param_ast = list->child[i]; + CG(generic_scope)->visible_count = i + 1; + if (!param_ast->child[2]) { + continue; + } + + CG(generic_scope)->self_compiling = ¶ms->parameters[i]; + params->parameters[i].default_type = zend_compile_typename(param_ast->child[2]); + if (zend_type_ast_has_generic_content(param_ast->child[2])) { + params->parameters[i].default_pre_erasure = zend_compile_pre_erasure_typename(param_ast->child[2]); + } + + if (ZEND_TYPE_IS_SET(params->parameters[i].bound) + && zend_check_generic_arg_satisfies_bound( + NULL, params->parameters[i].default_type, + NULL, params->parameters[i].bound) + == INHERITANCE_ERROR) { + zend_type bound_display = ZEND_TYPE_IS_SET(params->parameters[i].bound_pre_erasure) + ? params->parameters[i].bound_pre_erasure + : params->parameters[i].bound; + zend_type default_display = ZEND_TYPE_IS_SET(params->parameters[i].default_pre_erasure) + ? params->parameters[i].default_pre_erasure + : params->parameters[i].default_type; + zend_string *bound_str = zend_type_to_string(bound_display); + zend_string *default_str = zend_type_to_string(default_display); + zend_error_noreturn(E_COMPILE_ERROR, + "Default %s for type parameter %s does not satisfy its bound %s", + ZSTR_VAL(default_str), ZSTR_VAL(params->parameters[i].name), ZSTR_VAL(bound_str)); + } + + CG(generic_scope)->self_compiling = NULL; + } + + zend_generic_scope_pop(); + + return params; +} +/* }}} */ + +/* Returns true if the AST contains any generic-aware constructs (a type-parameter + * reference or a named type with type arguments) that need pre-erasure capture. */ +static bool zend_type_ast_has_generic_content(zend_ast *ast) +{ + if (!ast) { + return false; + } + zend_ast_attr orig = ast->attr; + ast->attr &= ~ZEND_TYPE_NULLABLE; + bool result = false; + + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + result = true; + } else if (ast->kind == ZEND_AST_TYPE_UNION || ast->kind == ZEND_AST_TYPE_INTERSECTION) { + zend_ast_list *list = zend_ast_get_list(ast); + for (uint32_t i = 0; i < list->children; i++) { + if (zend_type_ast_has_generic_content(list->child[i])) { + result = true; + break; + } + } + } else if (ast->kind == ZEND_AST_ZVAL) { + if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING && zend_generic_lookup(Z_STR_P(zv))) { + result = true; + } + } + } + + ast->attr = orig; + return result; +} + +static void zend_reject_typearg_on_iterable(zend_ast *ast) +{ + if (ast->kind != ZEND_AST_GENERIC_NAMED_TYPE) { + return; + } + + zend_ast *name_ast = ast->child[0]; + if (name_ast->kind != ZEND_AST_ZVAL) { + return; + } + + zval *zv = zend_ast_get_zval(name_ast); + if (Z_TYPE_P(zv) != IS_STRING) { + return; + } + + if ((name_ast->attr & ZEND_NAME_NOT_FQ) != ZEND_NAME_NOT_FQ) { + return; + } + + if (zend_string_equals_literal_ci(Z_STR_P(zv), "iterable")) { + zend_error_noreturn(E_COMPILE_ERROR, "Type arguments are not allowed on iterable"); + } +} + +/* Build a pre-erasure zend_type from a type-expression AST. The returned type + * may carry type-parameter references (TYPE_PARAMETER bit) or named-with-args + * payloads (NAMED_WITH_ARGS bit). Used only for the side table; the runtime + * never sees this form. */ +static zend_type zend_compile_pre_erasure_typename(zend_ast *ast) +{ + zend_reject_typearg_on_iterable(ast); + bool is_marked_nullable = ast->attr & ZEND_TYPE_NULLABLE; + zend_ast_attr orig_attr = ast->attr; + if (is_marked_nullable) { + ast->attr &= ~ZEND_TYPE_NULLABLE; + } + + zend_type result = ZEND_TYPE_INIT_NONE(0); + + if (ast->kind == ZEND_AST_TYPE_UNION || ast->kind == ZEND_AST_TYPE_INTERSECTION) { + zend_ast_list *list = zend_ast_get_list(ast); + zend_type_list *type_list = emalloc(ZEND_TYPE_LIST_SIZE(list->children)); + type_list->num_types = list->children; + for (uint32_t i = 0; i < list->children; i++) { + type_list->types[i] = zend_compile_pre_erasure_typename(list->child[i]); + } + ZEND_TYPE_SET_PTR(result, type_list); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_LIST_BIT | + (ast->kind == ZEND_AST_TYPE_UNION ? _ZEND_TYPE_UNION_BIT : _ZEND_TYPE_INTERSECTION_BIT); + } else if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_ast *name_ast = ast->child[0]; + zend_ast_list *args_list = zend_ast_get_list(ast->child[1]); + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(args_list->children)); + if (name_ast->kind == ZEND_AST_TYPE) { + ZEND_ASSERT(name_ast->attr == IS_STATIC); + payload->name = zend_string_init_interned(ZEND_STRL("static"), 0); + payload->name_attr = 0; + } else { + zend_string *raw = zval_make_interned_string(zend_ast_get_zval(name_ast)); + if (zend_get_class_fetch_type(raw) != ZEND_FETCH_CLASS_DEFAULT) { + payload->name = zend_string_copy(raw); + } else { + payload->name = zend_resolve_class_name(raw, name_ast->attr); + } + + payload->name_attr = name_ast->attr; + } + + payload->count = args_list->children; + for (uint32_t i = 0; i < args_list->children; i++) { + payload->args[i] = zend_compile_pre_erasure_typename(args_list->child[i]); + } + + ZEND_TYPE_SET_PTR(result, payload); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + } else if (ast->kind == ZEND_AST_TYPE) { + /* Builtin pseudo-type: same as erased. */ + result = (zend_type) ZEND_TYPE_INIT_CODE(ast->attr, 0, 0); + } else if (ast->kind == ZEND_AST_ZVAL) { + zend_generic_origin origin; + uint32_t index; + zend_generic_parameter *param = NULL; + if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING) { + param = zend_generic_lookup_full(Z_STR_P(zv), &origin, &index); + } + } + if (param) { + if (origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + zend_check_class_origin_in_static_context(param->name); + } + + zend_type_parameter_ref *ref = emalloc(sizeof(*ref)); + ref->name = zend_string_copy(param->name); + ref->index = index; + ref->origin = origin; + ZEND_TYPE_SET_PTR(result, ref); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_TYPE_PARAMETER_BIT; + } else { + if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING + && zend_generic_lookup_forward(Z_STR_P(zv)) >= 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s referenced before declaration", + Z_STRVAL_P(zv)); + } + } + zend_string *name = zval_make_interned_string(zend_ast_get_zval(ast)); + uint8_t code = zend_lookup_builtin_type_by_name(name); + if (code != 0) { + if (code == IS_ITERABLE) { + zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_MASK( + ZSTR_KNOWN(ZEND_STR_TRAVERSABLE), + (MAY_BE_ARRAY|_ZEND_TYPE_ITERABLE_BIT)); + result = iterable; + } else { + result = (zend_type) ZEND_TYPE_INIT_CODE(code, 0, 0); + } + } else { + zend_string_addref(name); + result = (zend_type) ZEND_TYPE_INIT_CLASS(name, 0, 0); + } + } + } else { + ZEND_UNREACHABLE(); + } + + if (is_marked_nullable) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + ast->attr = orig_attr; + return result; +} + +/* Ensure op_array->generic_types is allocated, then return it. */ +static zend_generic_type_table *zend_generic_get_or_create_op_array_table(zend_op_array *op_array) +{ + if (!op_array->generic_types) { + op_array->generic_types = zend_generic_type_table_alloc(); + } + return op_array->generic_types; +} + +/* Ensure ce->generic_types is allocated, then return it. */ +static zend_generic_type_table *zend_generic_get_or_create_class_table(zend_class_entry *ce) +{ + if (!ce->generic_types) { + ce->generic_types = zend_generic_type_table_alloc(); + } + return ce->generic_types; +} + +static zend_generic_parameter *zend_generic_lookup_name(zend_ast *ast) /* {{{ */ +{ + if (!CG(generic_scope) || ast->kind != ZEND_AST_ZVAL) { + return NULL; + } + if ((ast->attr & ZEND_NAME_NOT_FQ) != ZEND_NAME_NOT_FQ) { + return NULL; + } + zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) != IS_STRING) { + return NULL; + } + return zend_generic_lookup(Z_STR_P(zv)); } /* }}} */ @@ -1223,10 +1863,23 @@ static zend_string *zend_resolve_class_name(zend_string *name, uint32_t type) /* static zend_string *zend_resolve_class_name_ast(zend_ast *ast) /* {{{ */ { + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + ast = ast->child[0]; + } + const zval *class_name = zend_ast_get_zval(ast); if (Z_TYPE_P(class_name) != IS_STRING) { zend_error_noreturn(E_COMPILE_ERROR, "Illegal class name"); } + + if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ + && zend_generic_lookup(Z_STR_P(class_name))) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot use generic type parameter %s as a class reference at runtime; " + "bound-erased generic types have no runtime representation", + Z_STRVAL_P(class_name)); + } + return zend_resolve_class_name(Z_STR_P(class_name), ast->attr); } /* }}} */ @@ -1464,6 +2117,33 @@ zend_string *zend_type_to_string_resolved(const zend_type type, const zend_class str = add_type_string(str, resolved, /* is_intersection */ false); zend_string_release(resolved); } ZEND_TYPE_LIST_FOREACH_END(); + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); + smart_str buf = {0}; + if (nwa->name) { + zend_string *resolved = resolve_class_name(nwa->name, scope); + smart_str_append(&buf, resolved); + zend_string_release(resolved); + } + + smart_str_appendc(&buf, '<'); + for (uint32_t i = 0; i < nwa->count; i++) { + if (i > 0) smart_str_appends(&buf, ", "); + zend_string *piece = zend_type_to_string_resolved(nwa->args[i], scope); + if (piece) { + smart_str_append(&buf, piece); + zend_string_release(piece); + } + } + + smart_str_appendc(&buf, '>'); + smart_str_0(&buf); + str = buf.s; + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + if (ref->name) { + str = zend_string_copy(ref->name); + } } else if (ZEND_TYPE_HAS_NAME(type)) { str = resolve_class_name(ZEND_TYPE_NAME(type), scope); } @@ -1771,6 +2451,9 @@ static uint32_t zend_get_class_fetch_type_ast(zend_ast *name_ast) /* {{{ */ static zend_string *zend_resolve_const_class_name_reference(zend_ast *ast, const char *type) { + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + ast = ast->child[0]; + } zend_string *class_name = zend_ast_get_str(ast); if (ZEND_FETCH_CLASS_DEFAULT != zend_get_class_fetch_type_ast(ast)) { zend_error_noreturn(E_COMPILE_ERROR, @@ -2048,6 +2731,12 @@ int ZEND_FASTCALL zendlex(zend_parser_stack_elem *elem) /* {{{ */ zval zv; int ret; + if (CG(type_arg_residual_token)) { + ret = CG(type_arg_residual_token); + CG(type_arg_residual_token) = 0; + return ret; + } + if (CG(increment_lineno)) { CG(zend_lineno)++; CG(increment_lineno) = 0; @@ -2055,8 +2744,22 @@ int ZEND_FASTCALL zendlex(zend_parser_stack_elem *elem) /* {{{ */ ret = lex_scan(&zv, elem); ZEND_ASSERT(!EG(exception) || ret == T_ERROR); - return ret; + if (CG(type_arg_depth) > 0) { + switch (ret) { + case T_SR: + CG(type_arg_residual_token) = '>'; + return '>'; + case T_IS_GREATER_OR_EQUAL: + CG(type_arg_residual_token) = '='; + return '>'; + case T_SR_EQUAL: + CG(type_arg_residual_token) = T_IS_GREATER_OR_EQUAL; + return '>'; + } + } + + return ret; } /* }}} */ @@ -2090,6 +2793,8 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand ce->attributes = NULL; ce->enum_backing_type = IS_UNDEF; ce->backed_enum_table = NULL; + ce->generic_parameters = NULL; + ce->generic_types = NULL; if (nullify_handlers) { ce->constructor = NULL; @@ -2800,6 +3505,9 @@ static inline bool zend_can_write_to_variable(const zend_ast *ast) /* {{{ */ static inline bool zend_is_const_default_class_ref(zend_ast *name_ast) /* {{{ */ { + if (name_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + name_ast = name_ast->child[0]; + } if (name_ast->kind != ZEND_AST_ZVAL) { return false; } @@ -2856,6 +3564,11 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f { uint32_t fetch_type; + /* Generic named type: discard type arguments and resolve the bare name. */ + if (name_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + name_ast = name_ast->child[0]; + } + if (name_ast->kind != ZEND_AST_ZVAL) { znode name_node; @@ -4004,11 +4717,13 @@ ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, const zend_function *f } /* }}} */ -static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const zend_function *fbc, uint32_t lineno, uint32_t type) /* {{{ */ +static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const zend_function *fbc, uint32_t lineno, uint32_t type, zend_ast *turbofish_ast, uint8_t verify_kind, const znode *new_result) /* {{{ */ { zend_op *opline; uint32_t opnum_init = get_next_op_number() - 1; + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result); + if (args_ast->kind == ZEND_AST_CALLABLE_CONVERT) { opline = &CG(active_op_array)->opcodes[opnum_init]; opline->extended_value = 0; @@ -4090,7 +4805,7 @@ static bool zend_compile_function_name(znode *name_node, zend_ast *name_ast) /* } /* }}} */ -static void zend_compile_dynamic_call(znode *result, znode *name_node, zend_ast *args_ast, uint32_t lineno, uint32_t type) /* {{{ */ +static void zend_compile_dynamic_call(znode *result, znode *name_node, zend_ast *args_ast, zend_ast *turbofish_ast, uint32_t lineno, uint32_t type) /* {{{ */ { if (name_node->op_type == IS_CONST && Z_TYPE(name_node->u.constant) == IS_STRING) { const char *colon; @@ -4120,7 +4835,7 @@ static void zend_compile_dynamic_call(znode *result, znode *name_node, zend_ast zend_emit_op(NULL, ZEND_INIT_DYNAMIC_CALL, NULL, name_node); } - zend_compile_call_common(result, args_ast, NULL, lineno, type); + zend_compile_call_common(result, args_ast, NULL, lineno, type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL); } /* }}} */ @@ -4488,7 +5203,7 @@ static void zend_compile_assert(znode *result, zend_ast_list *args, zend_string args = (zend_ast_list *)zend_ast_list_add((zend_ast *) args, arg); } - zend_compile_call_common(result, (zend_ast*)args, fbc, lineno, type); + zend_compile_call_common(result, (zend_ast*)args, fbc, lineno, type, NULL, ZEND_VERIFY_ARITY_KIND_CALL, NULL); opline = &CG(active_op_array)->opcodes[check_op_number]; opline->op2.opline_num = get_next_op_number(); @@ -4814,7 +5529,7 @@ static uint32_t zend_compile_frameless_icall(znode *result, const zend_ast_list return zend_compile_frameless_icall_ex(result, args, fbc, frameless_function_info, type); } -static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast *args_ast, uint32_t lineno, uint32_t type) /* {{{ */ +static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast *args_ast, zend_ast *turbofish_ast, uint32_t lineno, uint32_t type) /* {{{ */ { int name_constants = zend_add_ns_func_name_literal(Z_STR(name_node->u.constant)); @@ -4849,7 +5564,7 @@ static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast opline->op2_type = IS_CONST; opline->op2.constant = name_constants; opline->result.num = zend_alloc_cache_slot(); - zend_compile_call_common(result, args_ast, NULL, lineno, type); + zend_compile_call_common(result, args_ast, NULL, lineno, type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL); /* Compile frameless call. */ if (frameless_function_info) { @@ -5141,7 +5856,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; @@ -5195,10 +5910,10 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg znode call_result; switch (callback->kind) { case ZEND_AST_CALL: - zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, callback->child[0], call_args)); + zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, callback->child[0], call_args, NULL)); break; case ZEND_AST_STATIC_CALL: - zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_STATIC_CALL, callback->child[0], callback->child[1], call_args)); + zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_STATIC_CALL, callback->child[0], callback->child[1], call_args, NULL)); break; } opline = zend_emit_op(NULL, ZEND_ADD_ARRAY_ELEMENT, &call_result, &key); @@ -5400,7 +6115,7 @@ static bool zend_compile_parent_property_hook_call(znode *result, const zend_ast opline->op1.constant = zend_add_literal_string(&property_name); opline->op2.num = hook_kind; - zend_compile_call_common(result, args_ast, NULL, zend_ast_get_lineno(method_ast), BP_VAR_R); + zend_compile_call_common(result, args_ast, NULL, zend_ast_get_lineno(method_ast), BP_VAR_R, NULL, ZEND_VERIFY_ARITY_KIND_CALL, NULL); return true; } @@ -5409,24 +6124,26 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) { zend_ast *name_ast = ast->child[0]; zend_ast *args_ast = ast->child[1]; + zend_ast *turbofish_ast = ast->child[2]; bool is_callable_convert = args_ast->kind == ZEND_AST_CALLABLE_CONVERT; znode name_node; if (name_ast->kind != ZEND_AST_ZVAL || Z_TYPE_P(zend_ast_get_zval(name_ast)) != IS_STRING) { zend_compile_expr(&name_node, name_ast); - zend_compile_dynamic_call(result, &name_node, args_ast, ast->lineno, type); + zend_compile_dynamic_call(result, &name_node, args_ast, turbofish_ast, ast->lineno, type); return; } { bool runtime_resolution = zend_compile_function_name(&name_node, name_ast); if (runtime_resolution) { - if (zend_string_equals_literal_ci(zend_ast_get_str(name_ast), "assert") + if (turbofish_ast == NULL + && zend_string_equals_literal_ci(zend_ast_get_str(name_ast), "assert") && !is_callable_convert) { zend_compile_assert(result, zend_ast_get_list(args_ast), Z_STR(name_node.u.constant), NULL, ast->lineno, type); } else { - zend_compile_ns_call(result, &name_node, args_ast, ast->lineno, type); + zend_compile_ns_call(result, &name_node, args_ast, turbofish_ast, ast->lineno, type); } return; } @@ -5440,7 +6157,8 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) zend_op *opline; /* Special assert() handling should apply independently of compiler flags. */ - if (fbc && zend_string_equals_literal(lcname, "assert") && !is_callable_convert) { + if (turbofish_ast == NULL + && fbc && zend_string_equals_literal(lcname, "assert") && !is_callable_convert) { zend_compile_assert(result, zend_ast_get_list(args_ast), lcname, fbc, ast->lineno, type); zend_string_release(lcname); zval_ptr_dtor(&name_node.u.constant); @@ -5451,13 +6169,14 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) || !fbc_is_finalized(fbc) || zend_compile_ignore_function(fbc, CG(active_op_array)->filename)) { zend_string_release_ex(lcname, 0); - zend_compile_dynamic_call(result, &name_node, args_ast, ast->lineno, type); + zend_compile_dynamic_call(result, &name_node, args_ast, turbofish_ast, ast->lineno, type); return; } - if (!is_callable_convert && - zend_try_compile_special_func(result, lcname, - zend_ast_get_list(args_ast), fbc, type, ast->lineno) == SUCCESS + if (turbofish_ast == NULL + && !is_callable_convert + && zend_try_compile_special_func(result, lcname, + zend_ast_get_list(args_ast), fbc, type, ast->lineno) == SUCCESS ) { zend_string_release_ex(lcname, 0); zval_ptr_dtor(&name_node.u.constant); @@ -5476,7 +6195,7 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) Z_EXTRA_P(CT_CONSTANT(opline->op2)) = fbc_bucket - CG(function_table)->arData; } - zend_compile_call_common(result, args_ast, fbc, ast->lineno, type); + zend_compile_call_common(result, args_ast, fbc, ast->lineno, type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL); } } /* }}} */ @@ -5486,6 +6205,7 @@ static void zend_compile_method_call(znode *result, zend_ast *ast, uint32_t type zend_ast *obj_ast = ast->child[0]; zend_ast *method_ast = ast->child[1]; zend_ast *args_ast = ast->child[2]; + zend_ast *turbofish_ast = ast->child[3]; znode obj_node, method_node; zend_op *opline; @@ -5540,7 +6260,7 @@ static void zend_compile_method_call(znode *result, zend_ast *ast, uint32_t type } } - if (zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast), type)) { + if (zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast), type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL)) { if (short_circuiting_checkpoint != zend_short_circuiting_checkpoint()) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot combine nullsafe operator with Closure creation"); @@ -5588,6 +6308,7 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type zend_ast *class_ast = ast->child[0]; zend_ast *method_ast = ast->child[1]; zend_ast *args_ast = ast->child[2]; + zend_ast *turbofish_ast = ast->child[3]; znode class_node, method_node; zend_op *opline; @@ -5655,7 +6376,7 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type } } - zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast), type); + zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast), type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL); } /* }}} */ @@ -5665,6 +6386,7 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ { zend_ast *class_ast = ast->child[0]; zend_ast *args_ast = ast->child[1]; + zend_ast *turbofish_ast = ast->child[2]; znode class_node, ctor_result; zend_op *opline; @@ -5711,7 +6433,7 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ fbc = ce->constructor; } - zend_compile_call_common(&ctor_result, args_ast, fbc, ast->lineno, BP_VAR_R); + zend_compile_call_common(&ctor_result, args_ast, fbc, ast->lineno, BP_VAR_R, turbofish_ast, ZEND_VERIFY_ARITY_KIND_NEW, result); zend_do_free(&ctor_result); } /* }}} */ @@ -6804,23 +7526,23 @@ static void zend_compile_pipe(znode *result, zend_ast *ast, uint32_t type) && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT && zend_is_pipe_optimizable_callable_name(callable_ast->child[0])) { fcall_ast = zend_ast_create(ZEND_AST_CALL, - callable_ast->child[0], arg_list_ast); + callable_ast->child[0], arg_list_ast, NULL); /* Turn $foo |> bar::baz(...) into bar::baz($foo). */ } else if (callable_ast->kind == ZEND_AST_STATIC_CALL && callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) { fcall_ast = zend_ast_create(ZEND_AST_STATIC_CALL, - callable_ast->child[0], callable_ast->child[1], arg_list_ast); + callable_ast->child[0], callable_ast->child[1], arg_list_ast, NULL); /* Turn $foo |> $bar->baz(...) into $bar->baz($foo). */ } else if (callable_ast->kind == ZEND_AST_METHOD_CALL && callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) { fcall_ast = zend_ast_create(ZEND_AST_METHOD_CALL, - callable_ast->child[0], callable_ast->child[1], arg_list_ast); + callable_ast->child[0], callable_ast->child[1], arg_list_ast, NULL); /* Turn $foo |> $expr into ($expr)($foo) */ } else { zend_compile_expr(&callable_result, callable_ast); callable_ast = zend_ast_create_znode(&callable_result); fcall_ast = zend_ast_create(ZEND_AST_CALL, - callable_ast, arg_list_ast); + callable_ast, arg_list_ast, NULL); } zend_do_extended_stmt(&operand_result); @@ -7361,9 +8083,80 @@ ZEND_API void zend_set_function_arg_flags(zend_function *func) /* {{{ */ } /* }}} */ +static zend_type_list *zend_arena_deep_copy_type_list(zend_type_list *src) +{ + size_t list_size = ZEND_TYPE_LIST_SIZE(src->num_types); + zend_type_list *copy = zend_arena_alloc(&CG(arena), list_size); + memcpy(copy, src, list_size); + for (uint32_t i = 0; i < copy->num_types; i++) { + if (ZEND_TYPE_HAS_LIST(copy->types[i])) { + ZEND_TYPE_SET_PTR(copy->types[i], zend_arena_deep_copy_type_list(ZEND_TYPE_LIST(copy->types[i]))); + } else if (ZEND_TYPE_HAS_NAME(copy->types[i])) { + zend_string_addref(ZEND_TYPE_NAME(copy->types[i])); + } + } + + return copy; +} + static zend_type zend_compile_single_typename(zend_ast *ast) { ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE)); + zend_reject_typearg_on_iterable(ast); + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + ast = ast->child[0]; + } + /* Generic type parameter reference: erase to the parameter's bound (or `mixed` when unbounded). */ + { + zend_generic_origin origin = ZEND_GENERIC_ORIGIN_CLASS_LIKE; + zend_generic_parameter *param = NULL; + if (CG(generic_scope) && ast->kind == ZEND_AST_ZVAL + && (ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING) { + param = zend_generic_lookup_full(Z_STR_P(zv), &origin, NULL); + } + } + + if (param) { + if (origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + zend_check_class_origin_in_static_context(param->name); + } + + for (zend_generic_scope_entry *e = CG(generic_scope); e; e = e->outer) { + if (e->self_compiling == param) { + zend_string *self = zend_string_copy(param->name); + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s cannot reference itself in its own bound or default outside of a generic type argument", + ZSTR_VAL(self)); + } + } + + if (ZEND_TYPE_IS_SET(param->bound)) { + zend_type result = param->bound; + if (ZEND_TYPE_HAS_NAME(result)) { + zend_string_addref(ZEND_TYPE_NAME(result)); + } else if (ZEND_TYPE_HAS_LIST(result)) { + ZEND_TYPE_SET_PTR(result, zend_arena_deep_copy_type_list(ZEND_TYPE_LIST(result))); + } + + return result; + } + + return (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ANY); + } + + if (ast->kind == ZEND_AST_ZVAL + && (ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING + && zend_generic_lookup_forward(Z_STR_P(zv)) >= 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s referenced before declaration", + Z_STRVAL_P(zv)); + } + } + } 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, @@ -7538,6 +8331,7 @@ static zend_type zend_compile_typename_ex( zend_ast *ast, bool force_allow_null, bool *forced_allow_null) /* {{{ */ { bool is_marked_nullable = ast->attr & ZEND_TYPE_NULLABLE; + bool is_t_ref_erasure = false; zend_ast_attr orig_ast_attr = ast->attr; zend_type type = ZEND_TYPE_INIT_NONE(0); @@ -7550,6 +8344,7 @@ static zend_type zend_compile_typename_ex( zend_type_list *type_list; bool is_composite = false; bool has_only_iterable_class = true; + bool collapse_union_to_mixed = false; ALLOCA_FLAG(use_heap) type_list = do_alloca(ZEND_TYPE_LIST_SIZE(list->children), use_heap); @@ -7595,6 +8390,16 @@ static zend_type zend_compile_typename_ex( uint32_t single_type_mask = ZEND_TYPE_PURE_MASK(single_type); if (single_type_mask == MAY_BE_ANY) { + zend_ast *check_ast = type_ast; + if (check_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + check_ast = check_ast->child[0]; + } + + if (zend_generic_lookup_name(check_ast) != NULL) { + collapse_union_to_mixed = true; + break; + } + zend_error_noreturn(E_COMPILE_ERROR, "Type mixed can only be used as a standalone type"); } if (ZEND_TYPE_IS_COMPLEX(single_type) && !ZEND_TYPE_IS_ITERABLE_FALLBACK(single_type)) { @@ -7641,25 +8446,30 @@ static zend_type zend_compile_typename_ex( } } - if (type_list->num_types) { - zend_type_list *list = zend_arena_alloc( - &CG(arena), ZEND_TYPE_LIST_SIZE(type_list->num_types)); - memcpy(list, type_list, ZEND_TYPE_LIST_SIZE(type_list->num_types)); - ZEND_TYPE_SET_LIST(type, list); - ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_ARENA_BIT; - /* Inform that the type list is a union type */ - ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_UNION_BIT; - } + if (collapse_union_to_mixed) { + free_alloca(type_list, use_heap); + type = (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ANY); + } else { + if (type_list->num_types) { + zend_type_list *list = zend_arena_alloc( + &CG(arena), ZEND_TYPE_LIST_SIZE(type_list->num_types)); + memcpy(list, type_list, ZEND_TYPE_LIST_SIZE(type_list->num_types)); + ZEND_TYPE_SET_LIST(type, list); + ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_ARENA_BIT; + /* Inform that the type list is a union type */ + ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_UNION_BIT; + } - free_alloca(type_list, use_heap); + free_alloca(type_list, use_heap); - uint32_t type_mask = ZEND_TYPE_FULL_MASK(type); - if ((type_mask & MAY_BE_OBJECT) && - ((!has_only_iterable_class && ZEND_TYPE_IS_COMPLEX(type)) || (type_mask & MAY_BE_STATIC))) { - zend_string *type_str = zend_type_to_string(type); - zend_error_noreturn(E_COMPILE_ERROR, - "Type %s contains both object and a class type, which is redundant", - ZSTR_VAL(type_str)); + uint32_t type_mask = ZEND_TYPE_FULL_MASK(type); + if ((type_mask & MAY_BE_OBJECT) && + ((!has_only_iterable_class && ZEND_TYPE_IS_COMPLEX(type)) || (type_mask & MAY_BE_STATIC))) { + zend_string *type_str = zend_type_to_string(type); + zend_error_noreturn(E_COMPILE_ERROR, + "Type %s contains both object and a class type, which is redundant", + ZSTR_VAL(type_str)); + } } } else if (ast->kind == ZEND_AST_TYPE_INTERSECTION) { const zend_ast_list *list = zend_ast_get_list(ast); @@ -7674,18 +8484,37 @@ static zend_type zend_compile_typename_ex( for (uint32_t i = 0; i < list->children; i++) { zend_ast *type_ast = list->child[i]; + zend_ast *check_ast = type_ast; + if (check_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + check_ast = check_ast->child[0]; + } + zend_generic_parameter *t_param = zend_generic_lookup_name(check_ast); zend_type single_type = zend_compile_single_typename(type_ast); /* 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)) { zend_string *standard_type_str = zend_type_to_string(single_type); + if (t_param) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s with bound %s cannot be part of an intersection type; " + "use an object-shaped bound (e.g. %s: object)", + ZSTR_VAL(t_param->name), ZSTR_VAL(standard_type_str), + ZSTR_VAL(t_param->name)); + } zend_error_noreturn(E_COMPILE_ERROR, "Type %s cannot be part of an intersection type", ZSTR_VAL(standard_type_str)); } /* An intersection of standard types cannot exist so invalidate it */ if (ZEND_TYPE_IS_ONLY_MASK(single_type)) { zend_string *standard_type_str = zend_type_to_string(single_type); + if (t_param) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s with bound %s cannot be part of an intersection type; " + "use an object-shaped bound (e.g. %s: object)", + ZSTR_VAL(t_param->name), ZSTR_VAL(standard_type_str), + ZSTR_VAL(t_param->name)); + } zend_error_noreturn(E_COMPILE_ERROR, "Type %s cannot be part of an intersection type", ZSTR_VAL(standard_type_str)); } @@ -7728,17 +8557,31 @@ static zend_type zend_compile_typename_ex( ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_ARENA_BIT; } } else { + zend_ast *check_ast = ast; + if (check_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + check_ast = check_ast->child[0]; + } + + is_t_ref_erasure = (zend_generic_lookup_name(check_ast) != NULL); type = zend_compile_single_typename(ast); } uint32_t type_mask = ZEND_TYPE_PURE_MASK(type); if (type_mask == MAY_BE_ANY && is_marked_nullable) { - zend_error_noreturn(E_COMPILE_ERROR, "Type mixed cannot be marked as nullable since mixed already includes null"); + if (!is_t_ref_erasure) { + zend_error_noreturn(E_COMPILE_ERROR, "Type mixed cannot be marked as nullable since mixed already includes null"); + } + + is_marked_nullable = false; } if ((type_mask & MAY_BE_NULL) && is_marked_nullable) { - zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable"); + if (!is_t_ref_erasure) { + zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable"); + } + + is_marked_nullable = false; } if (force_allow_null && !is_marked_nullable && !(type_mask & MAY_BE_NULL)) { @@ -7830,6 +8673,12 @@ static void zend_compile_attributes( ? ZEND_ATTRIBUTE_STRICT_TYPES : 0; attr = zend_add_attribute( attributes, name, args ? args->children : 0, flags, offset, el->lineno); + attr->generic_arity = (uint8_t) el->attr; + if (el->child[2]) { + zend_type compiled = zend_compile_turbofish_args_type(el->child[2]); + attr->generic_args = emalloc(sizeof(zend_type)); + *attr->generic_args = compiled; + } zend_string_release(name); /* Populate arguments */ @@ -8022,6 +8871,11 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 arg_infos->type = zend_compile_typename(return_type_ast); ZEND_TYPE_FULL_MASK(arg_infos->type) |= _ZEND_ARG_INFO_FLAGS( (op_array->fn_flags & ZEND_ACC_RETURN_REFERENCE) != 0, /* is_variadic */ 0, /* is_tentative */ 0); + if (zend_type_ast_has_generic_content(return_type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(return_type_ast); + zend_generic_type_table_set_return( + zend_generic_get_or_create_op_array_table(op_array), pre); + } } else { arg_infos->type = (zend_type) ZEND_TYPE_INIT_CODE(fallback_return_type, 0, 0); } @@ -8140,6 +8994,14 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 op_array->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS; arg_info->type = zend_compile_typename_ex(type_ast, force_nullable, &forced_allow_nullable); + if (zend_type_ast_has_generic_content(type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(type_ast); + if (force_nullable || forced_allow_nullable) { + ZEND_TYPE_FULL_MASK(pre) |= _ZEND_TYPE_NULLABLE_BIT; + } + zend_generic_type_table_set_parameter( + zend_generic_get_or_create_op_array_table(op_array), i, pre); + } if (forced_allow_nullable) { zend_string *func_name = get_function_or_method_name((zend_function *) op_array); zend_error(E_DEPRECATED, @@ -8238,6 +9100,11 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 zend_type type = ZEND_TYPE_INIT_NONE(0); if (type_ast) { type = zend_compile_typename(type_ast); + if (zend_type_ast_has_generic_content(type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(type_ast); + zend_generic_type_table_set_property( + zend_generic_get_or_create_class_table(scope), name, pre); + } } /* Don't give the property an explicit default value. For typed properties this means @@ -8738,6 +9605,7 @@ static zend_op_array *zend_compile_func_decl_ex( zend_ast *uses_ast = decl->child[1]; zend_ast *stmt_ast = decl->child[2]; zend_ast *return_type_ast = decl->child[3]; + zend_ast *generic_params_ast = decl->child[5]; bool is_method = decl->kind == ZEND_AST_METHOD; zend_string *lcname = NULL; bool is_hook = decl->kind == ZEND_AST_PROPERTY_HOOK; @@ -8840,8 +9708,23 @@ static zend_op_array *zend_compile_func_decl_ex( zend_stack_push(&CG(loop_var_stack), (void *) &dummy_var); } + bool save_in_static_member_type = CG(in_static_member_type); + CG(in_static_member_type) = is_method && (op_array->fn_flags & ZEND_ACC_STATIC); + + /* Harvest function/method generic type parameters and push them into scope + * so that parameter, return, and body type annotations erase correctly. + * See GENERICS.md §6.9. */ + if (generic_params_ast) { + op_array->generic_parameters = zend_compile_generic_type_parameter_list(generic_params_ast, ZEND_GENERIC_ORIGIN_FUNCTION_LIKE); + zend_generic_scope_push(op_array->generic_parameters, ZEND_GENERIC_ORIGIN_FUNCTION_LIKE); + } + zend_compile_params(params_ast, return_type_ast, is_method && zend_string_equals_literal(lcname, ZEND_TOSTRING_FUNC_LCNAME) ? IS_STRING : 0); + + zend_check_function_variance_markers(op_array); + + CG(in_static_member_type) = save_in_static_member_type; if (CG(active_op_array)->fn_flags & ZEND_ACC_GENERATOR) { zend_mark_function_as_generator(); zend_emit_op(NULL, ZEND_GENERATOR_CREATE, NULL, NULL); @@ -8923,6 +9806,10 @@ static zend_op_array *zend_compile_func_decl_ex( zend_string_release_ex(lcname, 0); } + if (op_array->generic_parameters) { + zend_generic_scope_pop(); + } + CG(active_op_array) = orig_op_array; CG(active_class_entry) = orig_class_entry; @@ -9184,8 +10071,19 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f } if (type_ast) { + bool save_in_static_member_type = CG(in_static_member_type); + if (flags & ZEND_ACC_STATIC) { + CG(in_static_member_type) = true; + } + type = zend_compile_typename(type_ast); + if (zend_type_ast_has_generic_content(type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(type_ast); + zend_generic_type_table_set_property( + zend_generic_get_or_create_class_table(ce), name, pre); + } + CG(in_static_member_type) = save_in_static_member_type; if (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_VOID|MAY_BE_NEVER|MAY_BE_CALLABLE)) { zend_string *str = zend_type_to_string(type); zend_error_noreturn(E_COMPILE_ERROR, @@ -9439,15 +10337,24 @@ static void zend_compile_use_trait(const zend_ast *ast) /* {{{ */ zend_ast *trait_ast = traits->child[i]; if (ce->ce_flags & ZEND_ACC_INTERFACE) { - zend_string *name = zend_ast_get_str(trait_ast); + zend_string *name = trait_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE + ? zend_ast_get_str(trait_ast->child[0]) + : zend_ast_get_str(trait_ast); zend_error_noreturn(E_COMPILE_ERROR, "Cannot use traits inside of interfaces. " "%s is used in %s", ZSTR_VAL(name), ZSTR_VAL(ce->name)); } - ce->trait_names[ce->num_traits].name = + uint32_t trait_index = ce->num_traits; + ce->trait_names[trait_index].name = zend_resolve_const_class_name_reference(trait_ast, "trait name"); - ce->trait_names[ce->num_traits].lc_name = zend_string_tolower(ce->trait_names[ce->num_traits].name); + ce->trait_names[trait_index].lc_name = zend_string_tolower(ce->trait_names[trait_index].name); ce->num_traits++; + + if (trait_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type pre = zend_compile_pre_erasure_typename(trait_ast); + zend_generic_type_table_set_trait_use( + zend_generic_get_or_create_class_table(ce), trait_index, pre); + } } if (!adaptations) { @@ -9483,6 +10390,12 @@ static void zend_compile_implements(zend_ast *ast) /* {{{ */ interface_names[i].name = zend_resolve_const_class_name_reference(class_ast, "interface name"); interface_names[i].lc_name = zend_string_tolower(interface_names[i].name); + + if (class_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type pre = zend_compile_pre_erasure_typename(class_ast); + zend_generic_type_table_set_implements( + zend_generic_get_or_create_class_table(ce), i, pre); + } } ce->num_interfaces = list->children; @@ -9573,6 +10486,20 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top } zend_register_seen_symbol(lcname, ZEND_SYMBOL_CLASS); + + /* If a generic scope is active, this declaration shadows any generic + * type parameter with the same (case-insensitive) name. Register the + * unqualified lc_name in the innermost scope so subsequent lookups + * resolve to the class instead of the type parameter. */ + if (CG(generic_scope)) { + zend_string *unqualified_lc = zend_string_tolower(unqualified_name); + if (!CG(generic_scope)->shadowing_classes) { + ALLOC_HASHTABLE(CG(generic_scope)->shadowing_classes); + zend_hash_init(CG(generic_scope)->shadowing_classes, 4, NULL, NULL, 0); + } + zend_hash_add_empty_element(CG(generic_scope)->shadowing_classes, unqualified_lc); + zend_string_release(unqualified_lc); + } } else { /* Find an anon class name that is not in use yet. */ name = NULL; @@ -9613,9 +10540,24 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top ce->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; } + if (decl->child[5]) { + ZEND_ASSERT(!(decl->flags & ZEND_ACC_ANON_CLASS)); + ce->generic_parameters = zend_compile_generic_type_parameter_list(decl->child[5], ZEND_GENERIC_ORIGIN_CLASS_LIKE); + zend_generic_scope_push(ce->generic_parameters, ZEND_GENERIC_ORIGIN_CLASS_LIKE); + } + if (extends_ast) { ce->parent_name = zend_resolve_const_class_name_reference(extends_ast, "class name"); + /* Capture the pre-erasure type when the extends clause specifies type + * arguments (e.g. `extends Box`). For an interface declaration + * this AST holds the list of parent interfaces; for class/trait it's + * the single parent class. */ + if (extends_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type pre = zend_compile_pre_erasure_typename(extends_ast); + zend_generic_type_table_set_extends( + zend_generic_get_or_create_class_table(ce), pre); + } } CG(active_class_entry) = ce; @@ -9645,6 +10587,12 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top zend_verify_abstract_class(ce); } + zend_check_generic_variance_markers(ce); + + if (ce->generic_parameters) { + zend_generic_scope_pop(); + } + CG(active_class_entry) = original_ce; if (toplevel) { @@ -11220,7 +12168,7 @@ static void zend_compile_shell_exec(znode *result, const zend_ast *ast) /* {{{ * ZVAL_STRING(&fn_name, "shell_exec"); name_ast = zend_ast_create_zval(&fn_name); args_ast = zend_ast_create_list(1, ZEND_AST_ARG_LIST, expr_ast); - call_ast = zend_ast_create(ZEND_AST_CALL, name_ast, args_ast); + call_ast = zend_ast_create(ZEND_AST_CALL, name_ast, args_ast, NULL); zend_compile_expr(result, call_ast); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 2351882a560d..2b9302d8e57f 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -121,6 +121,81 @@ typedef struct _zend_file_context { HashTable seen_symbols; } zend_file_context; +/* Maximum number of generic type parameters or arguments at any single + * position. Capped at 7 bits so a uint8_t count leaves its top bit + * available for future runtime-model flags. */ +#define ZEND_GENERIC_MAX_PARAMS 127 + +C23_ENUM(zend_generic_variance, uint8_t) { + ZEND_GENERIC_VARIANCE_INVARIANT = 0, + ZEND_GENERIC_VARIANCE_COVARIANT = 1, + ZEND_GENERIC_VARIANCE_CONTRAVARIANT = 2, +}; + +C23_ENUM(zend_generic_origin, uint8_t) { + ZEND_GENERIC_ORIGIN_CLASS_LIKE = 0, /* class, interface, trait, enum */ + ZEND_GENERIC_ORIGIN_FUNCTION_LIKE = 1, /* function, method, closure, arrow function */ +}; + +typedef struct _zend_generic_parameter { + zend_string *name; /* type-parameter name */ + zend_generic_variance variance; + zend_type bound; /* runtime erased; ZEND_TYPE_NONE if unbounded */ + zend_type bound_pre_erasure; /* pre-erasure; ZEND_TYPE_NONE if same as `bound` */ + zend_type default_type; /* runtime erased; ZEND_TYPE_NONE if no default */ + zend_type default_pre_erasure; /* pre-erasure; ZEND_TYPE_NONE if same as `default_type` */ +} zend_generic_parameter; + +typedef struct _zend_generic_parameter_list { + uint32_t count; + zend_generic_parameter parameters[1]; +} zend_generic_parameter_list; + +#define ZEND_GENERIC_PARAMETER_LIST_SIZE(count) \ + (sizeof(zend_generic_parameter_list) + ((count) - 1) * sizeof(zend_generic_parameter)) + +typedef struct _zend_generic_type_table { + zend_type *return_type; /* function/method return; NULL if equal to erased */ + zend_type *extends; /* class extends; NULL if equal */ + HashTable *parameters; /* parameter index -> zend_type * */ + HashTable *properties; /* zend_string * -> zend_type * */ + HashTable *class_constants; /* zend_string * -> zend_type * */ + HashTable *implements; /* implements index -> zend_type * */ + HashTable *trait_uses; /* trait-use index -> zend_type * */ + HashTable *turbofish_args; /* opline->extended_value -> zend_type * (NAMED_WITH_ARGS holding the call-site type arguments); index is stable across optimizer reorderings */ + bool persisted; /* set by opcache when the table lives in SHM/file-cache memory; suppresses destruction */ +} zend_generic_type_table; + +/* Compile-time linked stack of in-scope generic type parameters. */ +typedef struct _zend_generic_scope_entry { + zend_generic_parameter_list *params; + uint32_t visible_count; /* number of parameters in `params` already declared */ + struct _zend_generic_parameter *self_compiling; /* param whose bound/default is being compiled, or NULL */ + HashTable *shadowing_classes; /* lc_names of class-likes declared inside this scope; lazy-allocated */ + zend_generic_origin origin; + struct _zend_generic_scope_entry *outer; +} zend_generic_scope_entry; + +ZEND_API zend_generic_parameter_list *zend_generic_parameter_list_alloc(uint32_t count, bool persistent); +ZEND_API void zend_generic_parameter_list_destroy(zend_generic_parameter_list *list); +ZEND_API zend_generic_type_table *zend_generic_type_table_alloc(void); +ZEND_API void zend_generic_type_table_destroy(zend_generic_type_table *table); +ZEND_API void zend_generic_type_table_set_return(zend_generic_type_table *t, zend_type type); +ZEND_API void zend_generic_type_table_set_extends(zend_generic_type_table *t, zend_type type); +ZEND_API void zend_generic_type_table_set_parameter(zend_generic_type_table *t, uint32_t idx, zend_type type); +ZEND_API void zend_generic_type_table_set_property(zend_generic_type_table *t, zend_string *name, zend_type type); +ZEND_API void zend_generic_type_table_set_class_constant(zend_generic_type_table *t, zend_string *name, zend_type type); +ZEND_API void zend_generic_type_table_set_implements(zend_generic_type_table *t, uint32_t idx, zend_type type); +ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, uint32_t idx, zend_type type); +ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type); + +ZEND_API void zend_check_generic_param_list_size(zend_ast *list_ast); +ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast); + +ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box); +ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint32_t arity, const zend_type *args_box); +ZEND_API const zend_type *zend_generic_get_turbofish_args(const zend_op_array *caller_op_array, uint32_t args_id); + typedef union _zend_parser_stack_elem { zend_ast *ast; zend_string *str; @@ -260,7 +335,7 @@ typedef struct _zend_oparray_context { /* has #[\Override] attribute | | | */ #define ZEND_ACC_OVERRIDE (1 << 28) /* | X | X | */ /* | | | */ -/* Property Flags (unused: 13-27,29...) | | | */ +/* Property Flags (unused: 14-27,29...) | | | */ /* =========== | | | */ /* | | | */ /* Promoted property / parameter | | | */ @@ -274,6 +349,9 @@ typedef struct _zend_oparray_context { #define ZEND_ACC_PROTECTED_SET (1 << 11) /* | | X | */ #define ZEND_ACC_PRIVATE_SET (1 << 12) /* | | X | */ /* | | | */ +/* Parametric LSP substitution clone owned by a child | | | */ +#define ZEND_ACC_GENERIC_CLONE (1 << 13) /* | | X | */ +/* | | | */ /* Class Flags (unused: 31) | | | */ /* =========== | | | */ /* | | | */ @@ -576,6 +654,10 @@ struct _zend_op_array { * referenced by index from opcodes. */ zend_op_array **dynamic_func_defs; + /* Generic-syntax metadata. NULL on non-generic functions/methods. */ + zend_generic_parameter_list *generic_parameters; + zend_generic_type_table *generic_types; + void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 8257df32e831..421daeac8c3c 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -164,6 +164,25 @@ struct _zend_compiler_globals { uint32_t internal_run_time_cache_size; zend_stack short_circuiting_opnums; + + uint32_t type_arg_depth; + int type_arg_residual_token; + + struct _zend_generic_scope_entry *generic_scope; + + /* Set while compiling a type expression that has no instance binding for + * the enclosing class's type parameters (i.e. a static method's + * parameter/return types or a static property's type). Class-origin type + * parameter references resolved in this context are a compile error. */ + bool in_static_member_type; + + struct _zend_inheritance_binding_cache *inheritance_binding_cache; + + struct { + zend_class_entry *target; + const zend_type *args; + uint32_t arity; + } inheritance_binding_hint; #ifdef ZTS uint32_t copied_functions_count; #endif diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 8df6a5599d30..08eeef781fcb 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -34,6 +34,20 @@ ZEND_API zend_class_entry* (*zend_inheritance_cache_get)(zend_class_entry *ce, zend_class_entry *parent, zend_class_entry **traits_and_interfaces) = NULL; ZEND_API zend_class_entry* (*zend_inheritance_cache_add)(zend_class_entry *ce, zend_class_entry *proto, zend_class_entry *parent, zend_class_entry **traits_and_interfaces, HashTable *dependencies) = NULL; +static void zend_check_generic_link_arity( + const zend_class_entry *target_ce, uint32_t arity, + const char *clause, zend_string *child_name); +static void zend_check_generic_link_bounds( + zend_class_entry *target_ce, const zend_type *args_box, + const char *clause, zend_class_entry *ce); +static void zend_substitute_trait_method_arg_info( + zend_function *new_fn, const zend_function *orig_fn, + const zend_type *bind_args, uint32_t bind_arity); +static zend_arg_info *zend_clone_arg_info_block( + const zend_arg_info *orig_block, uint32_t total); +static bool zend_diamond_types_equal(zend_type a, zend_type b); +static const zend_type_named_with_args *zend_get_implements_binding(const zend_class_entry *ce, uint32_t idx); + /* Unresolved means that class declarations that are currently not available are needed to * determine whether the inheritance is valid or not. At runtime UNRESOLVED should be treated * as an ERROR. */ @@ -296,7 +310,7 @@ static zend_class_entry *lookup_class_ex( } /* The current class may not be registered yet, so check for it explicitly. */ - if (zend_string_equals_ci(scope->name, name)) { + if (scope && zend_string_equals_ci(scope->name, name)) { return scope; } } @@ -372,7 +386,9 @@ static bool zend_type_permits_self( const zend_type *single_type; ZEND_TYPE_FOREACH(type, single_type) { if (ZEND_TYPE_HAS_NAME(*single_type)) { - zend_string *name = resolve_class_name(scope, ZEND_TYPE_NAME(*single_type)); + zend_string *name = scope + ? resolve_class_name(scope, ZEND_TYPE_NAME(*single_type)) + : ZEND_TYPE_NAME(*single_type); const zend_class_entry *ce = lookup_class(self, name); if (ce && unlinked_instanceof(self, ce)) { return true; @@ -440,8 +456,9 @@ static inheritance_status zend_is_intersection_subtype_of_class( zend_class_entry *fe_ce; zend_string *fe_class_name = NULL; if (ZEND_TYPE_HAS_NAME(*single_type)) { - fe_class_name = - resolve_class_name(fe_scope, ZEND_TYPE_NAME(*single_type)); + fe_class_name = fe_scope + ? resolve_class_name(fe_scope, ZEND_TYPE_NAME(*single_type)) + : ZEND_TYPE_NAME(*single_type); if (zend_string_equals_ci(fe_class_name, proto_class_name)) { return INHERITANCE_SUCCESS; } @@ -543,8 +560,9 @@ static inheritance_status zend_is_class_subtype_of_type( zend_class_entry *proto_ce; zend_string *proto_class_name = NULL; if (ZEND_TYPE_HAS_NAME(*single_type)) { - proto_class_name = - resolve_class_name(proto_scope, ZEND_TYPE_NAME(*single_type)); + proto_class_name = proto_scope + ? resolve_class_name(proto_scope, ZEND_TYPE_NAME(*single_type)) + : ZEND_TYPE_NAME(*single_type); if (zend_string_equals_ci(fe_class_name, proto_class_name)) { if (!is_intersection) { return INHERITANCE_SUCCESS; @@ -585,7 +603,9 @@ static inheritance_status zend_is_class_subtype_of_type( static zend_string *get_class_from_type(const zend_class_entry *scope, const zend_type single_type) { if (ZEND_TYPE_HAS_NAME(single_type)) { - return resolve_class_name(scope, ZEND_TYPE_NAME(single_type)); + return scope + ? resolve_class_name(scope, ZEND_TYPE_NAME(single_type)) + : ZEND_TYPE_NAME(single_type); } return NULL; } @@ -675,6 +695,7 @@ static inheritance_status zend_perform_covariant_type_check( zend_class_entry *proto_scope, const zend_type proto_type) { ZEND_ASSERT(ZEND_TYPE_IS_SET(fe_type) && ZEND_TYPE_IS_SET(proto_type)); + ZEND_ASSERT((fe_scope == NULL) == (proto_scope == NULL)); /* Apart from void, everything is trivially covariant to the mixed type. * Handle this case separately to ensure it never requires class loading. */ @@ -756,21 +777,365 @@ static inheritance_status zend_perform_covariant_type_check( return early_exit_status == INHERITANCE_ERROR ? INHERITANCE_SUCCESS : INHERITANCE_ERROR; } - register_unresolved_classes(fe_scope, fe_type); - register_unresolved_classes(proto_scope, proto_type); + if (fe_scope) { + register_unresolved_classes(fe_scope, fe_type); + register_unresolved_classes(proto_scope, proto_type); + } + return INHERITANCE_UNRESOLVED; } +/* Direct binding only: the args ce supplies at its own extends/implements + * site. For transitive lookups, use zend_get_inheritance_binding_full. */ +static bool zend_get_inheritance_binding( + const zend_class_entry *ce, + const zend_class_entry *target_ce, + const zend_type **out_args, + uint32_t *out_arity) +{ + if (!ce->generic_types) { + return false; + } + + if (ce->generic_types->extends && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + const zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + bool matches = (ce->ce_flags & ZEND_ACC_RESOLVED_PARENT) + ? ce->parent == target_ce + : (named->name && target_ce->name && zend_string_equals_ci(named->name, target_ce->name)); + + if (matches) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } + + if (ce->generic_types->implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) continue; + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (zend_string_equals_ci(named->name, target_ce->name)) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + if (ce->generic_types->trait_uses) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->trait_uses, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) continue; + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (named->name && zend_string_equals_ci(named->name, target_ce->name)) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + return false; +} + +/* Substitutes a bare class-scope T-ref with its bound argument. */ +static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *args, uint32_t arity) +{ + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { + return t; + } + + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE || ref->index >= arity) { + return t; + } + + zend_type result = args[ref->index]; + if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + + return result; +} + +static bool zend_get_trait_use_binding( + const zend_class_entry *ce, uint32_t trait_index, + const zend_type **out_args, uint32_t *out_arity) +{ + if (!ce->generic_types || !ce->generic_types->trait_uses) return false; + zval *zv = zend_hash_index_find(ce->generic_types->trait_uses, trait_index); + if (!zv) return false; + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) return false; + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + *out_args = nwa->args; + *out_arity = nwa->count; + return true; +} + +static const zend_type *zend_get_trait_property_pre_erasure( + const zend_class_entry *trait_ce, zend_string *prop_name) +{ + if (!trait_ce->generic_types || !trait_ce->generic_types->properties) return NULL; + zval *zv = zend_hash_find(trait_ce->generic_types->properties, prop_name); + if (!zv) return NULL; + return (const zend_type *) Z_PTR_P(zv); +} + +static zend_class_entry *zend_find_interface_by_name( + const zend_class_entry *ce, const zend_string *name) +{ + /* ce->interfaces[] is only meaningful once resolution has populated it. */ + if (!(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES)) { + return NULL; + } + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + if (ce->interfaces[i] && zend_string_equals_ci(ce->interfaces[i]->name, name)) { + return ce->interfaces[i]; + } + } + return NULL; +} + +/* Walks the inheritance chain and composes substitutions when ce's binding + * for `target` is transitive. */ +ZEND_API bool zend_get_inheritance_binding_full( + const zend_class_entry *ce, + const zend_class_entry *target, + zend_type *out_args, + uint32_t out_capacity, + uint32_t *out_arity) +{ + const zend_type *direct_args; + if (zend_get_inheritance_binding(ce, target, &direct_args, out_arity)) { + if (*out_arity > out_capacity) return false; + for (uint32_t i = 0; i < *out_arity; i++) { + out_args[i] = direct_args[i]; + } + return true; + } + + if (!ce->generic_types) return false; + + uint32_t intermediate_cap = (target && target->generic_parameters) ? target->generic_parameters->count : 0; + if (intermediate_cap == 0) return false; + ALLOCA_FLAG(use_heap) + zend_type *intermediate_args = (zend_type *) do_alloca(sizeof(zend_type) * intermediate_cap, use_heap); + bool found = false; + + if (ce->generic_types->extends && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + zend_class_entry *parent_ce = (ce->ce_flags & ZEND_ACC_RESOLVED_PARENT) + ? ce->parent + : zend_lookup_class_ex( + ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends)->name, + NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + if (parent_ce) { + uint32_t intermediate_arity; + if (zend_get_inheritance_binding_full(parent_ce, target, intermediate_args, intermediate_cap, &intermediate_arity)) { + const zend_type *ce_to_parent; + uint32_t ce_to_parent_arity; + if (zend_get_inheritance_binding(ce, parent_ce, &ce_to_parent, &ce_to_parent_arity)) { + if (intermediate_arity > out_capacity) { + goto done; + } + + for (uint32_t i = 0; i < intermediate_arity; i++) { + out_args[i] = zend_substitute_leaf_type_param(intermediate_args[i], ce_to_parent, ce_to_parent_arity); + } + + *out_arity = intermediate_arity; + found = true; + goto done; + } + } + } + } + + if (ce->generic_types->implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) continue; + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + zend_class_entry *intermediate = zend_find_interface_by_name(ce, named->name); + if (!intermediate || intermediate == target) continue; + + uint32_t intermediate_arity; + if (!zend_get_inheritance_binding_full(intermediate, target, + intermediate_args, intermediate_cap, &intermediate_arity)) { + continue; + } + + if (intermediate_arity > out_capacity) { + goto done; + } + + for (uint32_t i = 0; i < intermediate_arity; i++) { + out_args[i] = zend_substitute_leaf_type_param( + intermediate_args[i], named->args, named->count); + } + *out_arity = intermediate_arity; + found = true; + goto done; + } ZEND_HASH_FOREACH_END(); + } + + done: + free_alloca(intermediate_args, use_heap); + return found; +} + +static bool zend_get_inheritance_binding_full_cached( + const zend_class_entry *ce, + const zend_class_entry *target, + zend_type *out_args, + uint32_t out_capacity, + uint32_t *out_arity) +{ + if (CG(inheritance_binding_hint).target == target && CG(inheritance_binding_hint).args) { + uint32_t arity = CG(inheritance_binding_hint).arity; + if (arity > out_capacity) { + return false; + } + + const zend_type *hint_args = CG(inheritance_binding_hint).args; + for (uint32_t i = 0; i < arity; i++) { + out_args[i] = hint_args[i]; + } + + *out_arity = arity; + return true; + } + + zend_inheritance_binding_cache *cache = CG(inheritance_binding_cache); + if (cache && cache->present && cache->ce == ce && cache->target == target) { + if (!cache->valid) { + return false; + } + + if (cache->arity > out_capacity) { + return false; + } + + for (uint32_t i = 0; i < cache->arity; i++) { + out_args[i] = cache->args[i]; + } + + *out_arity = cache->arity; + return true; + } + + bool result = zend_get_inheritance_binding_full( + ce, target, out_args, out_capacity, out_arity); + + if (cache) { + cache->ce = ce; + cache->target = target; + cache->present = true; + cache->valid = result; + if (result && *out_arity <= ZEND_GENERIC_MAX_PARAMS) { + cache->arity = *out_arity; + for (uint32_t i = 0; i < *out_arity; i++) { + cache->args[i] = out_args[i]; + } + } + } + + return result; +} + +/* Fills out_args with target_ce's parameter defaults if every parameter + * has one. */ +static bool zend_get_target_default_args( + const zend_class_entry *target_ce, + zend_type *out_args, uint32_t out_capacity, uint32_t *out_arity) +{ + if (!target_ce->generic_parameters) return false; + uint32_t total = target_ce->generic_parameters->count; + if (total > out_capacity) return false; + for (uint32_t i = 0; i < total; i++) { + zend_type d = target_ce->generic_parameters->parameters[i].default_type; + if (!ZEND_TYPE_IS_SET(d)) return false; + out_args[i] = d; + } + *out_arity = total; + return true; +} + +/* If ce supplies type arguments to proto's declaring scope (directly, + * transitively, or via parameter defaults), returns proto's pre-erasure type + * with class-scope T-refs substituted. */ +static zend_type zend_substitute_proto_type( + zend_type fallback, + const zend_type *pre_erasure, + const zend_function *proto, + zend_class_entry *ce) +{ + if (!ce || !pre_erasure) { + return fallback; + } + + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure) + || ZEND_TYPE_TYPE_PARAMETER(*pre_erasure)->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + return fallback; + } + + zend_class_entry *proto_scope = proto->common.scope; + if (!proto_scope || !proto_scope->generic_parameters) { + return fallback; + } + + uint32_t cap = proto_scope->generic_parameters->count; + if (cap == 0) { + return fallback; + } + + ALLOCA_FLAG(use_heap) + zend_type *args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t arity; + zend_type result; + if (!zend_get_inheritance_binding_full_cached(ce, proto_scope, args, cap, &arity) + && !zend_get_target_default_args(proto_scope, args, cap, &arity)) { + result = fallback; + } else { + zend_type substituted = zend_substitute_leaf_type_param(*pre_erasure, args, arity); + result = ZEND_TYPE_HAS_TYPE_PARAMETER(substituted) ? fallback : substituted; + } + + free_alloca(args, use_heap); + return result; +} + +static const zend_type *zend_get_param_pre_erasure(const zend_function *proto, uint32_t param_idx) +{ + if (!ZEND_USER_CODE(proto->common.type)) return NULL; + const zend_op_array *op_array = &proto->op_array; + if (!op_array->generic_types || !op_array->generic_types->parameters) return NULL; + return zend_hash_index_find_ptr(op_array->generic_types->parameters, param_idx); +} + +static const zend_type *zend_get_return_pre_erasure(const zend_function *proto) +{ + if (!ZEND_USER_CODE(proto->common.type)) return NULL; + const zend_op_array *op_array = &proto->op_array; + if (!op_array->generic_types) return NULL; + return op_array->generic_types->return_type; +} + static inheritance_status zend_do_perform_arg_type_hint_check( zend_class_entry *fe_scope, const zend_arg_info *fe_arg_info, - zend_class_entry *proto_scope, const zend_arg_info *proto_arg_info) /* {{{ */ + zend_class_entry *proto_scope, const zend_arg_info *proto_arg_info, + zend_type proto_type) /* {{{ */ { if (!ZEND_TYPE_IS_SET(fe_arg_info->type) || ZEND_TYPE_PURE_MASK(fe_arg_info->type) == MAY_BE_ANY) { /* Child with no type or mixed type is always compatible */ return INHERITANCE_SUCCESS; } - if (!ZEND_TYPE_IS_SET(proto_arg_info->type)) { + if (!ZEND_TYPE_IS_SET(proto_type)) { /* Child defines a type, but parent doesn't, violates LSP */ return INHERITANCE_ERROR; } @@ -778,7 +1143,7 @@ static inheritance_status zend_do_perform_arg_type_hint_check( /* Contravariant type check is performed as a covariant type check with swapped * argument order. */ return zend_perform_covariant_type_check( - proto_scope, proto_arg_info->type, fe_scope, fe_arg_info->type); + proto_scope, proto_type, fe_scope, fe_arg_info->type); } /* }}} */ @@ -787,7 +1152,8 @@ static inheritance_status zend_do_perform_arg_type_hint_check( * the method was declared in. */ static inheritance_status zend_do_perform_implementation_check( const zend_function *fe, zend_class_entry *fe_scope, - const zend_function *proto, zend_class_entry *proto_scope) /* {{{ */ + const zend_function *proto, zend_class_entry *proto_scope, + zend_class_entry *ce) /* {{{ */ { uint32_t num_args, proto_num_args, fe_num_args; inheritance_status status, local_status; @@ -848,8 +1214,15 @@ static inheritance_status zend_do_perform_implementation_check( return INHERITANCE_ERROR; } + uint32_t proto_param_idx = i < proto_num_args ? i : proto_num_args - 1; + zend_type proto_type = zend_substitute_proto_type( + proto_arg_info->type, + zend_get_param_pre_erasure(proto, proto_param_idx), + proto, + ce + ); local_status = zend_do_perform_arg_type_hint_check( - fe_scope, fe_arg_info, proto_scope, proto_arg_info); + fe_scope, fe_arg_info, proto_scope, proto_arg_info, proto_type); if (UNEXPECTED(local_status != INHERITANCE_SUCCESS)) { if (UNEXPECTED(local_status == INHERITANCE_ERROR)) { @@ -879,8 +1252,12 @@ static inheritance_status zend_do_perform_implementation_check( return status; } + zend_type proto_return_type = zend_substitute_proto_type( + proto->common.arg_info[-1].type, + zend_get_return_pre_erasure(proto), + proto, ce); local_status = zend_perform_covariant_type_check( - fe_scope, fe->common.arg_info[-1].type, proto_scope, proto->common.arg_info[-1].type); + fe_scope, fe->common.arg_info[-1].type, proto_scope, proto_return_type); if (UNEXPECTED(local_status != INHERITANCE_SUCCESS)) { if (local_status == INHERITANCE_ERROR @@ -896,10 +1273,11 @@ static inheritance_status zend_do_perform_implementation_check( /* }}} */ static ZEND_COLD void zend_append_type_hint( - smart_str *str, const zend_class_entry *scope, const zend_arg_info *arg_info, bool return_hint) /* {{{ */ + smart_str *str, const zend_class_entry *scope, const zend_arg_info *arg_info, + zend_type display_type, bool return_hint) /* {{{ */ { - if (ZEND_TYPE_IS_SET(arg_info->type)) { - zend_string *type_str = zend_type_to_string_resolved(arg_info->type, scope); + if (ZEND_TYPE_IS_SET(display_type)) { + zend_string *type_str = zend_type_to_string_resolved(display_type, scope); smart_str_append(str, type_str); zend_string_release(type_str); if (!return_hint) { @@ -910,7 +1288,8 @@ static ZEND_COLD void zend_append_type_hint( /* }}} */ static ZEND_COLD zend_string *zend_get_function_declaration( - const zend_function *fptr, const zend_class_entry *scope) /* {{{ */ + const zend_function *fptr, const zend_class_entry *scope, + zend_class_entry *subst_ce) /* {{{ */ { smart_str str = {0}; @@ -941,8 +1320,11 @@ static ZEND_COLD zend_string *zend_get_function_declaration( num_args++; } for (uint32_t i = 0; i < num_args;) { - zend_append_type_hint(&str, scope, arg_info, false); - + uint32_t param_idx = i < fptr->common.num_args ? i : fptr->common.num_args; + zend_type display_type = subst_ce + ? zend_substitute_proto_type(arg_info->type, zend_get_param_pre_erasure(fptr, param_idx), fptr, subst_ce) + : arg_info->type; + zend_append_type_hint(&str, scope, arg_info, display_type, false); if (ZEND_ARG_SEND_MODE(arg_info)) { smart_str_appendc(&str, '&'); } @@ -1036,7 +1418,11 @@ static ZEND_COLD zend_string *zend_get_function_declaration( if (fptr->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { smart_str_appends(&str, ": "); - zend_append_type_hint(&str, scope, fptr->common.arg_info - 1, true); + const zend_arg_info *ret_info = fptr->common.arg_info - 1; + zend_type ret_display = subst_ce + ? zend_substitute_proto_type(ret_info->type, zend_get_return_pre_erasure(fptr), fptr, subst_ce) + : ret_info->type; + zend_append_type_hint(&str, scope, ret_info, ret_display, true); } smart_str_0(&str); @@ -1056,8 +1442,11 @@ static void ZEND_COLD emit_incompatible_method_error( const zend_function *child, const zend_class_entry *child_scope, const zend_function *parent, const zend_class_entry *parent_scope, inheritance_status status) { - zend_string *parent_prototype = zend_get_function_declaration(parent, parent_scope); - zend_string *child_prototype = zend_get_function_declaration(child, child_scope); + /* When the child class binds the parent's type parameters (extends/implements + * with type args), display the parent signature in its substituted form so + * the message matches the form being checked. */ + zend_string *parent_prototype = zend_get_function_declaration(parent, parent_scope, (zend_class_entry *) child_scope); + zend_string *child_prototype = zend_get_function_declaration(child, child_scope, NULL); if (status == INHERITANCE_UNRESOLVED) { // TODO Improve error message if first unresolved class is present in child and parent? /* Fetch the first unresolved class from registered autoloads */ @@ -1099,7 +1488,7 @@ static void perform_delayable_implementation_check( const zend_function *proto, zend_class_entry *proto_scope) { inheritance_status status = - zend_do_perform_implementation_check(fe, fe_scope, proto, proto_scope); + zend_do_perform_implementation_check(fe, fe_scope, proto, proto_scope, ce); if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { if (EXPECTED(status == INHERITANCE_UNRESOLVED)) { add_compatibility_obligation(ce, fe, fe_scope, proto, proto_scope); @@ -1228,8 +1617,9 @@ static inheritance_status do_inheritance_check_on_method( if (flags & ZEND_INHERITANCE_CHECK_PROTO) { if (flags & ZEND_INHERITANCE_CHECK_SILENT) { - return zend_do_perform_implementation_check(child, child_scope, parent, parent_scope); + return zend_do_perform_implementation_check(child, child_scope, parent, parent_scope, ce); } + perform_delayable_implementation_check(ce, child, child_scope, parent, parent_scope); } @@ -1245,120 +1635,559 @@ static inheritance_status do_inheritance_check_on_method( } /* }}} */ -static void do_inherit_method(zend_string *key, zend_function *parent, zend_class_entry *ce, bool is_interface, uint32_t flags) /* {{{ */ +static zend_function *zend_maybe_substitute_inherited_method( + zend_function *parent_fn, zend_class_entry *ce) { - zval *child = zend_hash_find_known_hash(&ce->function_table, key); + if (parent_fn->type != ZEND_USER_FUNCTION) { + return NULL; + } - if (child) { - zend_function *func = (zend_function*)Z_PTR_P(child); + if (!parent_fn->common.scope || !parent_fn->common.scope->generic_parameters) { + return NULL; + } - if (is_interface && UNEXPECTED(func == parent)) { - /* The same method in interface may be inherited few times */ - return; - } + const zend_op_array *op = &parent_fn->op_array; + if (!op->generic_types) { + return NULL; + } - do_inheritance_check_on_method( - func, func->common.scope, parent, parent->common.scope, ce, child, flags); - } else { + if (!op->generic_types->parameters && !op->generic_types->return_type) { + return NULL; + } - if (is_interface || (parent->common.fn_flags & (ZEND_ACC_ABSTRACT))) { - ce->ce_flags |= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS; - } + zend_class_entry *defining_ce = parent_fn->common.scope; + uint32_t cap = defining_ce->generic_parameters->count; + if (cap == 0) { + return NULL; + } - parent = zend_duplicate_function(parent, ce); + ALLOCA_FLAG(use_heap) + zend_type *bound_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t bound_arity = 0; + bool have = zend_get_inheritance_binding_full_cached(ce, defining_ce, bound_args, cap, &bound_arity); - if (!is_interface) { - _zend_hash_append_ptr(&ce->function_table, key, parent); - } else { - zend_hash_add_new_ptr(&ce->function_table, key, parent); - } + if (!have) { + have = zend_get_target_default_args(defining_ce, bound_args, cap, &bound_arity); } -} -/* }}} */ -static inheritance_status full_property_types_compatible( - const zend_property_info *parent_info, const zend_property_info *child_info, - prop_variance variance) { - if (ZEND_TYPE_PURE_MASK(parent_info->type) == ZEND_TYPE_PURE_MASK(child_info->type) - && ZEND_TYPE_NAME(parent_info->type) == ZEND_TYPE_NAME(child_info->type)) { - return INHERITANCE_SUCCESS; + if (!have) { + free_alloca(bound_args, use_heap); + return NULL; } - if (ZEND_TYPE_IS_SET(parent_info->type) != ZEND_TYPE_IS_SET(child_info->type)) { - return INHERITANCE_ERROR; + zend_function *clone = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(clone, parent_fn, sizeof(zend_op_array)); + clone->op_array.fn_flags &= ~ZEND_ACC_IMMUTABLE; + + zend_substitute_trait_method_arg_info(clone, parent_fn, bound_args, bound_arity); + free_alloca(bound_args, use_heap); + if (clone->op_array.arg_info == parent_fn->op_array.arg_info) { + return NULL; } - /* Perform a covariant type check in both directions to determined invariance. */ - inheritance_status status1 = variance == PROP_CONTRAVARIANT ? INHERITANCE_SUCCESS : - zend_perform_covariant_type_check( - child_info->ce, child_info->type, parent_info->ce, parent_info->type); - inheritance_status status2 = variance == PROP_COVARIANT ? INHERITANCE_SUCCESS : - zend_perform_covariant_type_check( - parent_info->ce, parent_info->type, child_info->ce, child_info->type); - if (status1 == INHERITANCE_SUCCESS && status2 == INHERITANCE_SUCCESS) { - return INHERITANCE_SUCCESS; + function_add_ref(clone); + return clone; +} + +/* True iff `t` can be a component of an intersection: a class-name leaf, + * or an already-built intersection of class-name leaves. */ +static bool zend_type_intersectable(zend_type t) +{ + if (ZEND_TYPE_HAS_NAME(t)) { + return ZEND_TYPE_PURE_MASK(t) == 0 || (ZEND_TYPE_PURE_MASK(t) & ~MAY_BE_NULL) == 0; } - if (status1 == INHERITANCE_ERROR || status2 == INHERITANCE_ERROR) { - return INHERITANCE_ERROR; + + if (ZEND_TYPE_HAS_LIST(t) && (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_INTERSECTION_BIT)) { + return true; } - ZEND_ASSERT(status1 == INHERITANCE_UNRESOLVED || status2 == INHERITANCE_UNRESOLVED); - return INHERITANCE_UNRESOLVED; -} -static ZEND_COLD void emit_incompatible_property_error( - const zend_property_info *child, const zend_property_info *parent, prop_variance variance) { - zend_string *type_str = zend_type_to_string_resolved(parent->type, parent->ce); - zend_error_noreturn(E_COMPILE_ERROR, - "Type of %s::$%s must be %s%s (as in class %s)", - ZSTR_VAL(child->ce->name), - zend_get_unmangled_property_name(child->name), - variance == PROP_INVARIANT ? "" : - variance == PROP_COVARIANT ? "subtype of " : "supertype of ", - ZSTR_VAL(type_str), - ZSTR_VAL(parent->ce->name)); + return false; } -static ZEND_COLD void emit_set_hook_type_error(const zend_property_info *child, const zend_property_info *parent) +static ZEND_COLD ZEND_NORETURN void zend_diamond_uninhabited_intersection_error( + const zend_class_entry *ce, + const zend_class_entry *defining_ce, + const zend_function *fn, + bool is_return_slot, + uint32_t param_slot_idx, + zend_type a, zend_type b) { - zend_type set_type = parent->hooks[ZEND_PROPERTY_HOOK_SET]->common.arg_info[0].type; - zend_string *type_str = zend_type_to_string_resolved(set_type, parent->ce); - zend_error_noreturn(E_COMPILE_ERROR, - "Set type of %s::$%s must be supertype of %s (as in %s %s)", - ZSTR_VAL(child->ce->name), - zend_get_unmangled_property_name(child->name), - ZSTR_VAL(type_str), - zend_get_object_type_case(parent->ce, false), - ZSTR_VAL(parent->ce->name)); + zend_string *a_str = zend_type_to_string_resolved(a, (zend_class_entry *) defining_ce); + zend_string *b_str = zend_type_to_string_resolved(b, (zend_class_entry *) defining_ce); + if (is_return_slot) { + zend_error_noreturn(E_COMPILE_ERROR, + "Diamond inheritance of %s::%s() in %s would require return type %s&%s, which is uninhabited; constrain the type parameter with an object bound", + ZSTR_VAL(defining_ce->name), ZSTR_VAL(fn->common.function_name), + ZSTR_VAL(ce->name), ZSTR_VAL(a_str), ZSTR_VAL(b_str)); + } else { + zend_error_noreturn(E_COMPILE_ERROR, + "Diamond inheritance of %s::%s() in %s would require parameter #%u type %s&%s, which is uninhabited; constrain the type parameter with an object bound", + ZSTR_VAL(defining_ce->name), ZSTR_VAL(fn->common.function_name), + ZSTR_VAL(ce->name), param_slot_idx + 1, ZSTR_VAL(a_str), ZSTR_VAL(b_str)); + } } -static inheritance_status verify_property_type_compatibility( - const zend_property_info *parent_info, - const zend_property_info *child_info, - prop_variance variance, - bool throw_on_error, - bool throw_on_unresolved -) { - inheritance_status result = full_property_types_compatible(parent_info, child_info, variance); - if ((result == INHERITANCE_ERROR && throw_on_error) || (result == INHERITANCE_UNRESOLVED && throw_on_unresolved)) { - emit_incompatible_property_error(child_info, parent_info, variance); +static zend_type zend_synth_variance_merged_type(zend_type a, zend_type b, bool intersect) +{ + if (intersect) { + uint32_t total = 0; + if (ZEND_TYPE_HAS_LIST(a) && (ZEND_TYPE_FULL_MASK(a) & _ZEND_TYPE_INTERSECTION_BIT)) { + total += ZEND_TYPE_LIST(a)->num_types; + } else { + total += 1; + } + + if (ZEND_TYPE_HAS_LIST(b) && (ZEND_TYPE_FULL_MASK(b) & _ZEND_TYPE_INTERSECTION_BIT)) { + total += ZEND_TYPE_LIST(b)->num_types; + } else { + total += 1; + } + + zend_type_list *list = zend_arena_alloc(&CG(arena), ZEND_TYPE_LIST_SIZE(total)); + uint32_t idx = 0; + if (ZEND_TYPE_HAS_LIST(a) && (ZEND_TYPE_FULL_MASK(a) & _ZEND_TYPE_INTERSECTION_BIT)) { + const zend_type_list *al = ZEND_TYPE_LIST(a); + for (uint32_t k = 0; k < al->num_types; k++) { + list->types[idx] = al->types[k]; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + } else { + list->types[idx] = a; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + + if (ZEND_TYPE_HAS_LIST(b) && (ZEND_TYPE_FULL_MASK(b) & _ZEND_TYPE_INTERSECTION_BIT)) { + const zend_type_list *bl = ZEND_TYPE_LIST(b); + for (uint32_t k = 0; k < bl->num_types; k++) { + list->types[idx] = bl->types[k]; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + } else { + list->types[idx] = b; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + + list->num_types = idx; + zend_type result = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, list); + ZEND_TYPE_FULL_MASK(result) |= + _ZEND_TYPE_LIST_BIT | _ZEND_TYPE_ARENA_BIT | _ZEND_TYPE_INTERSECTION_BIT; + if (ZEND_TYPE_ALLOW_NULL(a) && ZEND_TYPE_ALLOW_NULL(b)) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + + return result; } - if (result != INHERITANCE_SUCCESS) { + + uint32_t mask = ZEND_TYPE_PURE_MASK(a) | ZEND_TYPE_PURE_MASK(b); + + const zend_type *a_complex_arr = NULL; + uint32_t a_complex_count = 0; + zend_type a_single = ZEND_TYPE_INIT_NONE(0); + if (ZEND_TYPE_HAS_LIST(a) && (ZEND_TYPE_FULL_MASK(a) & _ZEND_TYPE_UNION_BIT)) { + a_complex_arr = ZEND_TYPE_LIST(a)->types; + a_complex_count = ZEND_TYPE_LIST(a)->num_types; + } else if (ZEND_TYPE_HAS_NAME(a)) { + a_single = a; + a_single.type_mask &= ~_ZEND_TYPE_MAY_BE_MASK; + a_complex_arr = &a_single; + a_complex_count = 1; + } + + const zend_type *b_complex_arr = NULL; + uint32_t b_complex_count = 0; + zend_type b_single = ZEND_TYPE_INIT_NONE(0); + if (ZEND_TYPE_HAS_LIST(b) && (ZEND_TYPE_FULL_MASK(b) & _ZEND_TYPE_UNION_BIT)) { + b_complex_arr = ZEND_TYPE_LIST(b)->types; + b_complex_count = ZEND_TYPE_LIST(b)->num_types; + } else if (ZEND_TYPE_HAS_NAME(b)) { + b_single = b; + b_single.type_mask &= ~_ZEND_TYPE_MAY_BE_MASK; + b_complex_arr = &b_single; + b_complex_count = 1; + } + + uint32_t total = a_complex_count + b_complex_count; + zend_type result = ZEND_TYPE_INIT_NONE(0); + if (total == 0) { + ZEND_TYPE_FULL_MASK(result) |= mask; return result; } - if (parent_info->flags & ZEND_ACC_ABSTRACT) { - ZEND_ASSERT(parent_info->hooks); - if (parent_info->hooks[ZEND_PROPERTY_HOOK_SET] - && (!child_info->hooks || !child_info->hooks[ZEND_PROPERTY_HOOK_SET])) { - zend_type set_type = parent_info->hooks[ZEND_PROPERTY_HOOK_SET]->common.arg_info[0].type; - inheritance_status result = zend_perform_covariant_type_check( - parent_info->ce, set_type, child_info->ce, child_info->type); - if ((result == INHERITANCE_ERROR && throw_on_error) || (result == INHERITANCE_UNRESOLVED && throw_on_unresolved)) { - emit_set_hook_type_error(child_info, parent_info); - } - } + + if (total == 1) { + const zend_type *only = a_complex_count ? a_complex_arr : b_complex_arr; + result = *only; + zend_type_copy_ctor(&result, /* use_arena */ true, /* persistent */ false); + ZEND_TYPE_FULL_MASK(result) |= mask; + return result; } - return INHERITANCE_SUCCESS; -} + + zend_type_list *list = zend_arena_alloc(&CG(arena), ZEND_TYPE_LIST_SIZE(total)); + list->num_types = total; + uint32_t idx = 0; + for (uint32_t k = 0; k < a_complex_count; k++) { + list->types[idx] = a_complex_arr[k]; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + + for (uint32_t k = 0; k < b_complex_count; k++) { + list->types[idx] = b_complex_arr[k]; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + + ZEND_TYPE_SET_PTR(result, list); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_LIST_BIT | _ZEND_TYPE_ARENA_BIT | _ZEND_TYPE_UNION_BIT | mask; + return result; +} + +/* Declared covariant merges as intersection; contravariant as union; invariant + * falls back to use-site variance (return: intersection, param: union). + * Returns true and writes `*intersect_out` when `pre` is a class-origin T-ref. */ +static bool zend_generic_merge_polarity( + const zend_class_entry *defining_ce, + const zend_type *pre, + bool is_return_slot, + bool *intersect_out) +{ + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pre)) { + return false; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pre); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE + || ref->index >= defining_ce->generic_parameters->count) { + return false; + } + switch (defining_ce->generic_parameters->parameters[ref->index].variance) { + case ZEND_GENERIC_VARIANCE_COVARIANT: + *intersect_out = true; + return true; + case ZEND_GENERIC_VARIANCE_CONTRAVARIANT: + *intersect_out = false; + return true; + default: + *intersect_out = is_return_slot; + return true; + } +} + +static bool zend_iface_merge_slot_decision( + const zend_class_entry *defining_ce, + const zend_type *pre, + zend_type existing_type, + zend_type incoming_type, + const zend_type *hint_args, uint32_t hint_arity, + bool is_return_slot, + zend_type *b_out, bool *intersect_out) +{ + if (!zend_generic_merge_polarity(defining_ce, pre, is_return_slot, intersect_out)) { + return false; + } + + zend_type b = incoming_type; + if (hint_args) { + zend_type substituted = zend_substitute_leaf_type_param(*pre, hint_args, hint_arity); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(substituted)) { + b = substituted; + } + } + + if (zend_diamond_types_equal(existing_type, b)) { + return false; + } + + *b_out = b; + return true; +} + +static zend_function *zend_iface_build_merged_clone( + zend_function *existing, zend_function *incoming, + zend_class_entry *ce) +{ + if (!(ce->ce_flags & ZEND_ACC_INTERFACE) + || existing->type != ZEND_USER_FUNCTION + || incoming->type != ZEND_USER_FUNCTION + || existing->common.scope != incoming->common.scope) { + return NULL; + } + + zend_class_entry *defining_ce = existing->common.scope; + if (!defining_ce || !defining_ce->generic_parameters) { + return NULL; + } + + const zend_op_array *eop = &existing->op_array; + if (!eop->generic_types) { + return NULL; + } + + if (existing->common.num_args != incoming->common.num_args + || (existing->common.fn_flags & ZEND_ACC_VARIADIC) + != (incoming->common.fn_flags & ZEND_ACC_VARIADIC)) { + return NULL; + } + + uint32_t num_args = existing->common.num_args + ((existing->common.fn_flags & ZEND_ACC_VARIADIC) ? 1 : 0); + bool has_return = (existing->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = num_args + (has_return ? 1 : 0); + if (total == 0) { + return NULL; + } + + const zend_arg_info *e_block = has_return ? existing->op_array.arg_info - 1 : existing->op_array.arg_info; + const zend_arg_info *i_block = has_return ? incoming->op_array.arg_info - 1 : incoming->op_array.arg_info; + uint32_t return_slot_offset = has_return ? 1 : 0; + + const zend_type *hint_args = NULL; + uint32_t hint_arity = 0; + if (CG(inheritance_binding_hint).target == defining_ce) { + hint_args = CG(inheritance_binding_hint).args; + hint_arity = CG(inheritance_binding_hint).arity; + } + + bool any_needs_merge = false; + if (has_return && eop->generic_types->return_type) { + zend_type b; bool intersect; + any_needs_merge = zend_iface_merge_slot_decision( + defining_ce, eop->generic_types->return_type, + e_block[0].type, i_block[0].type, + hint_args, hint_arity, /* is_return_slot */ true, + &b, &intersect); + } + + if (!any_needs_merge && eop->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(eop->generic_types->parameters, idx, zv) { + if (idx >= num_args) continue; + zend_type b; bool intersect; + if (zend_iface_merge_slot_decision( + defining_ce, (const zend_type *) Z_PTR_P(zv), + e_block[return_slot_offset + idx].type, + i_block[return_slot_offset + idx].type, + hint_args, hint_arity, /* is_return_slot */ false, + &b, &intersect)) { + any_needs_merge = true; + break; + } + } ZEND_HASH_FOREACH_END(); + + (void) idx; + } + + if (!any_needs_merge) { + return NULL; + } + + zend_arg_info *new_block = zend_clone_arg_info_block(e_block, total); + + if (has_return && eop->generic_types->return_type) { + zend_type b; bool intersect; + if (zend_iface_merge_slot_decision( + defining_ce, eop->generic_types->return_type, + e_block[0].type, i_block[0].type, + hint_args, hint_arity, /* is_return_slot */ true, + &b, &intersect)) { + if (intersect && (!zend_type_intersectable(new_block[0].type) || !zend_type_intersectable(b))) { + zend_diamond_uninhabited_intersection_error( + ce, defining_ce, existing, + /* is_return_slot */ true, 0, + new_block[0].type, b); + } + + new_block[0].type = zend_synth_variance_merged_type(new_block[0].type, b, intersect); + } + } + + if (eop->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(eop->generic_types->parameters, idx, zv) { + if (idx >= num_args) continue; + zend_type b; bool intersect; + if (!zend_iface_merge_slot_decision( + defining_ce, (const zend_type *) Z_PTR_P(zv), + e_block[return_slot_offset + idx].type, + i_block[return_slot_offset + idx].type, + hint_args, hint_arity, /* is_return_slot */ false, + &b, &intersect)) { + continue; + } + + uint32_t slot = return_slot_offset + idx; + if (intersect && (!zend_type_intersectable(new_block[slot].type) || !zend_type_intersectable(b))) { + zend_diamond_uninhabited_intersection_error( + ce, defining_ce, existing, + /* is_return_slot */ false, (uint32_t) idx, + new_block[slot].type, b); + } + + new_block[slot].type = zend_synth_variance_merged_type(new_block[slot].type, b, intersect); + } ZEND_HASH_FOREACH_END(); + + (void) idx; + } + + zend_function *merged_fn = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(merged_fn, existing, sizeof(zend_op_array)); + merged_fn->op_array.fn_flags &= ~ZEND_ACC_IMMUTABLE; + merged_fn->op_array.arg_info = has_return ? new_block + 1 : new_block; + ZEND_MAP_PTR_INIT(merged_fn->op_array.run_time_cache, NULL); + ZEND_MAP_PTR_INIT(merged_fn->op_array.static_variables_ptr, NULL); + return merged_fn; +} + +static bool zend_iface_merge_generic_inherited_method( + zend_function *existing, zend_function *incoming, + zval *existing_zv, zend_class_entry *ce) +{ + zend_function *merged = zend_iface_build_merged_clone(existing, incoming, ce); + if (!merged) { + return false; + } + + Z_PTR_P(existing_zv) = merged; + return true; +} + +static bool zend_iface_merge_generic_inherited_hook( + zend_function *existing, zend_function *incoming, + zend_property_info *child_info, zend_property_hook_kind kind, + zend_class_entry *ce) +{ + zend_function *merged = zend_iface_build_merged_clone(existing, incoming, ce); + if (!merged) { + return false; + } + + child_info->hooks[kind] = merged; + return true; +} + +static void do_inherit_method(zend_string *key, zend_function *parent, zend_class_entry *ce, bool is_interface, uint32_t flags) /* {{{ */ +{ + zval *child = zend_hash_find_known_hash(&ce->function_table, key); + + if (child) { + zend_function *func = (zend_function*)Z_PTR_P(child); + + if (is_interface && UNEXPECTED(func == parent)) { + /* The same method in interface may be inherited few times */ + return; + } + + if (is_interface && zend_iface_merge_generic_inherited_method(func, parent, child, ce)) { + return; + } + + do_inheritance_check_on_method( + func, func->common.scope, parent, parent->common.scope, ce, child, flags); + } else { + + if (is_interface || (parent->common.fn_flags & (ZEND_ACC_ABSTRACT))) { + ce->ce_flags |= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS; + } + + zend_function *substituted = zend_maybe_substitute_inherited_method(parent, ce); + parent = substituted ? substituted : zend_duplicate_function(parent, ce); + + if (!is_interface) { + _zend_hash_append_ptr(&ce->function_table, key, parent); + } else { + zend_hash_add_new_ptr(&ce->function_table, key, parent); + } + } +} +/* }}} */ + +static inheritance_status full_property_types_compatible( + const zend_property_info *parent_info, const zend_property_info *child_info, + prop_variance variance) { + if (ZEND_TYPE_PURE_MASK(parent_info->type) == ZEND_TYPE_PURE_MASK(child_info->type) + && ZEND_TYPE_NAME(parent_info->type) == ZEND_TYPE_NAME(child_info->type)) { + return INHERITANCE_SUCCESS; + } + + if (ZEND_TYPE_IS_SET(parent_info->type) != ZEND_TYPE_IS_SET(child_info->type)) { + return INHERITANCE_ERROR; + } + + /* Perform a covariant type check in both directions to determined invariance. */ + inheritance_status status1 = variance == PROP_CONTRAVARIANT ? INHERITANCE_SUCCESS : + zend_perform_covariant_type_check( + child_info->ce, child_info->type, parent_info->ce, parent_info->type); + inheritance_status status2 = variance == PROP_COVARIANT ? INHERITANCE_SUCCESS : + zend_perform_covariant_type_check( + parent_info->ce, parent_info->type, child_info->ce, child_info->type); + if (status1 == INHERITANCE_SUCCESS && status2 == INHERITANCE_SUCCESS) { + return INHERITANCE_SUCCESS; + } + if (status1 == INHERITANCE_ERROR || status2 == INHERITANCE_ERROR) { + return INHERITANCE_ERROR; + } + ZEND_ASSERT(status1 == INHERITANCE_UNRESOLVED || status2 == INHERITANCE_UNRESOLVED); + return INHERITANCE_UNRESOLVED; +} + +static ZEND_COLD void emit_incompatible_property_error( + const zend_property_info *child, const zend_property_info *parent, prop_variance variance) { + zend_string *type_str = zend_type_to_string_resolved(parent->type, parent->ce); + zend_error_noreturn(E_COMPILE_ERROR, + "Type of %s::$%s must be %s%s (as in class %s)", + ZSTR_VAL(child->ce->name), + zend_get_unmangled_property_name(child->name), + variance == PROP_INVARIANT ? "" : + variance == PROP_COVARIANT ? "subtype of " : "supertype of ", + ZSTR_VAL(type_str), + ZSTR_VAL(parent->ce->name)); +} + +static ZEND_COLD void emit_set_hook_type_error(const zend_property_info *child, const zend_property_info *parent) +{ + zend_type set_type = parent->hooks[ZEND_PROPERTY_HOOK_SET]->common.arg_info[0].type; + zend_string *type_str = zend_type_to_string_resolved(set_type, parent->ce); + zend_error_noreturn(E_COMPILE_ERROR, + "Set type of %s::$%s must be supertype of %s (as in %s %s)", + ZSTR_VAL(child->ce->name), + zend_get_unmangled_property_name(child->name), + ZSTR_VAL(type_str), + zend_get_object_type_case(parent->ce, false), + ZSTR_VAL(parent->ce->name)); +} + +static inheritance_status verify_property_type_compatibility( + const zend_property_info *parent_info, + const zend_property_info *child_info, + prop_variance variance, + bool throw_on_error, + bool throw_on_unresolved +) { + inheritance_status result = full_property_types_compatible(parent_info, child_info, variance); + if ((result == INHERITANCE_ERROR && throw_on_error) || (result == INHERITANCE_UNRESOLVED && throw_on_unresolved)) { + emit_incompatible_property_error(child_info, parent_info, variance); + } + if (result != INHERITANCE_SUCCESS) { + return result; + } + if (parent_info->flags & ZEND_ACC_ABSTRACT) { + ZEND_ASSERT(parent_info->hooks); + if (parent_info->hooks[ZEND_PROPERTY_HOOK_SET] + && (!child_info->hooks || !child_info->hooks[ZEND_PROPERTY_HOOK_SET])) { + zend_type set_type = parent_info->hooks[ZEND_PROPERTY_HOOK_SET]->common.arg_info[0].type; + inheritance_status result = zend_perform_covariant_type_check( + parent_info->ce, set_type, child_info->ce, child_info->type); + if ((result == INHERITANCE_ERROR && throw_on_error) || (result == INHERITANCE_UNRESOLVED && throw_on_unresolved)) { + emit_set_hook_type_error(child_info, parent_info); + } + } + } + return INHERITANCE_SUCCESS; +} static bool property_has_operation(const zend_property_info *prop_info, zend_property_hook_kind kind) { @@ -1418,6 +2247,10 @@ static void inherit_property_hook( ZSTR_VAL(parent->common.function_name)); } + if ((ce->ce_flags & ZEND_ACC_INTERFACE) && zend_iface_merge_generic_inherited_hook(child, parent, child_info, kind, ce)) { + return; + } + do_inheritance_check_on_method( child, child->common.scope, parent, parent->common.scope, ce, /* child */ NULL, ZEND_INHERITANCE_CHECK_PROTO | ZEND_INHERITANCE_CHECK_VISIBILITY @@ -1573,7 +2406,69 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke } } - _zend_hash_append_ptr(&ce->properties_info, key, parent_info); + zend_property_info *info = parent_info; + if (parent_info->ce->generic_types && parent_info->ce->generic_types->properties) { + zval *zv = zend_hash_find(parent_info->ce->generic_types->properties, key); + if (zv && parent_info->ce->generic_parameters && parent_info->ce->generic_parameters->count > 0) { + const zend_type *pre_erasure = (const zend_type *) Z_PTR_P(zv); + uint32_t cap = parent_info->ce->generic_parameters->count; + ALLOCA_FLAG(use_heap) + zend_type *bound_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t bound_arity = 0; + bool have_args = zend_get_inheritance_binding_full_cached(ce, parent_info->ce, bound_args, cap, &bound_arity); + if (!have_args) { + have_args = zend_get_target_default_args(parent_info->ce, bound_args, cap, &bound_arity); + } + + zend_type sub = have_args + ? zend_substitute_leaf_type_param(*pre_erasure, bound_args, bound_arity) + : (zend_type) ZEND_TYPE_INIT_NONE(0); + free_alloca(bound_args, use_heap); + if (have_args) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + zend_property_info *clone = zend_arena_alloc(&CG(arena), sizeof(*clone)); + *clone = *parent_info; + clone->flags |= ZEND_ACC_GENERIC_CLONE; + clone->type = sub; + zend_type_copy_ctor(&clone->type, /* use_arena */ true, /* persistent */ false); + + if (hooks && (hooks[ZEND_PROPERTY_HOOK_GET] || hooks[ZEND_PROPERTY_HOOK_SET])) { + zend_function **clone_hooks = zend_arena_alloc( + &CG(arena), ZEND_PROPERTY_HOOK_STRUCT_SIZE); + memcpy(clone_hooks, hooks, ZEND_PROPERTY_HOOK_STRUCT_SIZE); + + for (uint32_t hi = 0; hi < ZEND_PROPERTY_HOOK_COUNT; hi++) { + zend_function *orig = hooks[hi]; + if (!orig) continue; + + uint32_t num_args = orig->op_array.num_args; + if (orig->op_array.fn_flags & ZEND_ACC_VARIADIC) num_args++; + uint32_t total = num_args + 1; + + zend_arg_info *new_arg_info = zend_clone_arg_info_block(orig->op_array.arg_info - 1, total); + + uint32_t slot = (hi == ZEND_PROPERTY_HOOK_GET) ? 0 : 1; + new_arg_info[slot].type = sub; + zend_type_copy_ctor(&new_arg_info[slot].type, /* use_arena */ true, /* persistent */ false); + + zend_function *clone_fn = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(clone_fn, orig, sizeof(zend_op_array)); + clone_fn->op_array.arg_info = new_arg_info + 1; + function_add_ref(clone_fn); + + clone_hooks[hi] = clone_fn; + } + + clone->hooks = clone_hooks; + } + + info = clone; + } + } + } + } + + _zend_hash_append_ptr(&ce->properties_info, key, info); } } /* }}} */ @@ -1838,6 +2733,28 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par zend_property_info *property_info; zend_string *key; + zend_inheritance_binding_cache binding_cache; + binding_cache.present = false; + zend_inheritance_binding_cache *prev_binding_cache = CG(inheritance_binding_cache); + CG(inheritance_binding_cache) = &binding_cache; + + if (parent_ce && !(ce->ce_flags & ZEND_ACC_INTERFACE)) { + const zend_type *extends_args = NULL; + uint32_t arity = 0; + if (ce->generic_types + && ce->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + extends_args = ce->generic_types->extends; + arity = ZEND_TYPE_NAMED_WITH_ARGS(*extends_args)->count; + } + + if (arity > 0 || parent_ce->generic_parameters) { + zend_check_generic_link_arity(parent_ce, arity, "extends", ce->name); + } + + zend_check_generic_link_bounds(parent_ce, extends_args, "extends", ce); + } + if (UNEXPECTED(ce->ce_flags & ZEND_ACC_INTERFACE)) { /* Interface can only inherit other interfaces */ if (UNEXPECTED(!(parent_ce->ce_flags & ZEND_ACC_INTERFACE))) { @@ -2044,6 +2961,8 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par } } ce->ce_flags |= parent_ce->ce_flags & (ZEND_HAS_STATIC_IN_METHODS | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_READONLY_PROPS | ZEND_ACC_USE_GUARDS | ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES); + + CG(inheritance_binding_cache) = prev_binding_cache; } /* }}} */ @@ -2239,6 +3158,64 @@ ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry } /* }}} */ +static bool zend_iface_diamond_bindings_allowed( + const zend_class_entry *iface, + const zend_type *prior_args, uint32_t prior_arity, + const zend_type *cur_args, uint32_t cur_arity) +{ + if (!iface->generic_parameters || prior_arity != cur_arity) { + return false; + } + + for (uint32_t k = 0; k < prior_arity; k++) { + if (!zend_diamond_types_equal(prior_args[k], cur_args[k])) { + return true; + } + } + + return false; +} + +static bool zend_iface_diamond_direct_dup_allowed( + const zend_class_entry *ce, const zend_class_entry *iface, + uint32_t prior_clause_idx, uint32_t cur_clause_idx) +{ + const zend_type_named_with_args *prior = zend_get_implements_binding(ce, prior_clause_idx); + const zend_type_named_with_args *cur = zend_get_implements_binding(ce, cur_clause_idx); + if (!prior || !cur) { + return false; + } + + return zend_iface_diamond_bindings_allowed(iface, prior->args, prior->count, cur->args, cur->count); +} + +static bool zend_iface_diamond_parent_vs_own_allowed( + const zend_class_entry *ce, const zend_class_entry *iface, + uint32_t cur_clause_idx) +{ + if (!ce->parent || !iface->generic_parameters) { + return false; + } + + const zend_type_named_with_args *cur = zend_get_implements_binding(ce, cur_clause_idx); + if (!cur) { + return false; + } + + uint32_t cap = iface->generic_parameters->count; + if (cap == 0) { + return false; + } + + ALLOCA_FLAG(use_heap) + zend_type *prior_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t prior_arity; + bool allowed = zend_get_inheritance_binding_full(ce->parent, iface, prior_args, cap, &prior_arity) + && zend_iface_diamond_bindings_allowed(iface, prior_args, prior_arity, cur->args, cur->count); + free_alloca(prior_args, use_heap); + return allowed; +} + static void zend_do_implement_interfaces(zend_class_entry *ce, zend_class_entry **interfaces) /* {{{ */ { uint32_t num_parent_interfaces = ce->parent ? ce->parent->num_interfaces : 0; @@ -2259,12 +3236,20 @@ static void zend_do_implement_interfaces(zend_class_entry *ce, zend_class_entry for (uint32_t j = 0; j < num_interfaces; j++) { if (interfaces[j] == iface) { if (j >= num_parent_interfaces) { + if (zend_iface_diamond_direct_dup_allowed(ce, iface, j - num_parent_interfaces, i)) { + break; + } + efree(interfaces); zend_error_noreturn(E_COMPILE_ERROR, "%s %s cannot implement previously implemented interface %s", zend_get_object_type_uc(ce), ZSTR_VAL(ce->name), ZSTR_VAL(iface->name)); } + + if (zend_iface_diamond_parent_vs_own_allowed(ce, iface, i)) { + break; + } /* skip duplications */ ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&iface->constants_table, key, c) { do_inherit_constant_check(ce, c, key); @@ -2295,10 +3280,25 @@ static void zend_do_implement_interfaces(zend_class_entry *ce, zend_class_entry for (i = 0; i < num_parent_interfaces; i++) { do_implement_interface(ce, ce->interfaces[i]); } + /* Note that new interfaces can be added during this loop due to interface inheritance. * Use num_interfaces rather than ce->num_interfaces to not re-process the new ones. */ for (; i < num_interfaces; i++) { - do_interface_implementation(ce, ce->interfaces[i]); + zend_class_entry *iface_ce = ce->interfaces[i]; + const zend_type_named_with_args *binding = iface_ce->generic_parameters + ? zend_get_implements_binding(ce, i - num_parent_interfaces) + : NULL; + + if (binding) { + CG(inheritance_binding_hint).target = iface_ce; + CG(inheritance_binding_hint).args = binding->args; + CG(inheritance_binding_hint).arity = binding->count; + } + + do_interface_implementation(ce, iface_ce); + CG(inheritance_binding_hint).target = NULL; + CG(inheritance_binding_hint).args = NULL; + CG(inheritance_binding_hint).arity = 0; } } /* }}} */ @@ -2352,17 +3352,283 @@ static zend_class_entry *fixup_trait_scope(const zend_function *fn, zend_class_e return fn->common.scope->ce_flags & ZEND_ACC_TRAIT ? ce : fn->common.scope; } -static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_string *key, zend_function *fn) /* {{{ */ +static const zend_type_named_with_args *zend_find_trait_use_binding_by_name( + const zend_class_entry *ce, zend_string *trait_name) { - zend_function *existing_fn = NULL; - zend_function *new_fn; + if (!ce->generic_types || !ce->generic_types->trait_uses) return NULL; + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->trait_uses, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) continue; + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (nwa->name && zend_string_equals_ci(nwa->name, trait_name)) { + return nwa; + } + } ZEND_HASH_FOREACH_END(); - if ((existing_fn = zend_hash_find_ptr(&ce->function_table, key)) != NULL) { - /* if it is the same function with the same visibility and has not been assigned a class scope yet, regardless - * of where it is coming from there is no conflict and we do not need to add it again */ - if (existing_fn->op_array.opcodes == fn->op_array.opcodes && - (existing_fn->common.fn_flags & ZEND_ACC_PPP_MASK) == (fn->common.fn_flags & ZEND_ACC_PPP_MASK) && + return NULL; +} + +static zend_arg_info *zend_clone_arg_info_block(const zend_arg_info *orig_block, uint32_t total) +{ + zend_arg_info *new_block = zend_arena_alloc(&CG(arena), sizeof(zend_arg_info) * total); + memcpy(new_block, orig_block, sizeof(zend_arg_info) * total); + for (uint32_t i = 0; i < total; i++) { + if (new_block[i].name) { + zend_string_addref(new_block[i].name); + } + + if (new_block[i].doc_comment) { + zend_string_addref(new_block[i].doc_comment); + } + + zend_type_copy_ctor(&new_block[i].type, /* use_arena */ true, /* persistent */ false); + } + + return new_block; +} + +static void zend_substitute_trait_method_arg_info( + zend_function *new_fn, const zend_function *orig_fn, + const zend_type *bind_args, uint32_t bind_arity) +{ + if (orig_fn->type != ZEND_USER_FUNCTION) return; + const zend_op_array *orig_op = &orig_fn->op_array; + if (!orig_op->generic_types) { + return; + } + + if (!orig_op->generic_types->parameters && !orig_op->generic_types->return_type) { + return; + } + + uint32_t num_args = orig_op->num_args; + if (orig_op->fn_flags & ZEND_ACC_VARIADIC) num_args++; + bool has_return = (orig_op->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = num_args + (has_return ? 1 : 0); + if (total == 0) { + return; + } + + const zend_arg_info *orig_block = has_return ? orig_op->arg_info - 1 : orig_op->arg_info; + zend_arg_info *new_block = NULL; + + uint32_t return_slot_offset = has_return ? 1 : 0; + + if (has_return && orig_op->generic_types->return_type) { + const zend_type *pre = orig_op->generic_types->return_type; + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre)) { + zend_type sub = zend_substitute_leaf_type_param(*pre, bind_args, bind_arity); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + if (!new_block) { + new_block = zend_clone_arg_info_block(orig_block, total); + } + new_block[0].type = sub; + zend_type_copy_ctor(&new_block[0].type, /* use_arena */ true, /* persistent */ false); + } + } + } + + if (orig_op->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(orig_op->generic_types->parameters, idx, zv) { + if (idx >= num_args) { + continue; + } + + zend_type *pre_erasure = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure)) { + continue; + } + + zend_type sub = zend_substitute_leaf_type_param(*pre_erasure, bind_args, bind_arity); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + continue; + } + + if (!new_block) { + new_block = zend_clone_arg_info_block(orig_block, total); + } + new_block[return_slot_offset + idx].type = sub; + zend_type_copy_ctor(&new_block[return_slot_offset + idx].type, /* use_arena */ true, /* persistent */ false); + } ZEND_HASH_FOREACH_END(); + } + + if (new_block) { + new_fn->op_array.arg_info = has_return ? new_block + 1 : new_block; + } +} + +static const zend_type_named_with_args *zend_get_trait_use_binding_by_index( + const zend_class_entry *ce, uint32_t trait_idx) +{ + if (!ce->generic_types || !ce->generic_types->trait_uses) { + return NULL; + } + + zval *zv = zend_hash_index_find(ce->generic_types->trait_uses, trait_idx); + if (!zv) { + return NULL; + } + + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + return NULL; + } + + return ZEND_TYPE_NAMED_WITH_ARGS(*boxed); +} + +static void zend_trait_diamond_merge_method( + zend_class_entry *ce, zend_string *key, + zend_function *existing, zend_function *fn, + const zend_type_named_with_args *binding) +{ + if (!binding || existing->type != ZEND_USER_FUNCTION) { + return; + } + + zend_class_entry *defining_ce = existing->common.scope; + if (!defining_ce || !defining_ce->generic_parameters) { + return; + } + + const zend_op_array *eop = &existing->op_array; + if (!eop->generic_types) { + return; + } + + uint32_t num_args = existing->common.num_args + ((existing->common.fn_flags & ZEND_ACC_VARIADIC) ? 1 : 0); + bool has_return = (existing->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = num_args + (has_return ? 1 : 0); + if (total == 0) { + return; + } + + const zend_arg_info *e_block = has_return ? existing->op_array.arg_info - 1 : existing->op_array.arg_info; + uint32_t return_slot_offset = has_return ? 1 : 0; + + bool any_needs_merge = false; + if (has_return && eop->generic_types->return_type) { + const zend_type *pre = eop->generic_types->return_type; + bool intersect; + if (zend_generic_merge_polarity(defining_ce, pre, /* is_return_slot */ true, &intersect)) { + zend_type sub = zend_substitute_leaf_type_param(*pre, binding->args, binding->count); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub) && !zend_diamond_types_equal(e_block[0].type, sub)) { + any_needs_merge = true; + } + } + } + + if (!any_needs_merge && eop->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(eop->generic_types->parameters, idx, zv) { + if (idx >= num_args) { + continue; + } + + const zend_type *pre = (const zend_type *) Z_PTR_P(zv); + bool intersect; + if (!zend_generic_merge_polarity(defining_ce, pre, /* is_return_slot */ false, &intersect)) { + continue; + } + + zend_type sub = zend_substitute_leaf_type_param(*pre, binding->args, binding->count); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub) && !zend_diamond_types_equal(e_block[return_slot_offset + idx].type, sub)) { + any_needs_merge = true; + break; + } + } ZEND_HASH_FOREACH_END(); + + (void) idx; + } + + if (!any_needs_merge) { + return; + } + + zend_arg_info *new_block = zend_clone_arg_info_block(e_block, total); + + if (has_return && eop->generic_types->return_type) { + const zend_type *pre = eop->generic_types->return_type; + bool intersect; + if (zend_generic_merge_polarity(defining_ce, pre, /* is_return_slot */ true, &intersect)) { + zend_type sub = zend_substitute_leaf_type_param(*pre, binding->args, binding->count); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub) && !zend_diamond_types_equal(new_block[0].type, sub)) { + if (intersect && (!zend_type_intersectable(new_block[0].type) || !zend_type_intersectable(sub))) { + zend_diamond_uninhabited_intersection_error( + ce, defining_ce, existing, + /* is_return_slot */ true, 0, + new_block[0].type, sub); + } + + new_block[0].type = zend_synth_variance_merged_type(new_block[0].type, sub, intersect); + } + } + } + + if (eop->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(eop->generic_types->parameters, idx, zv) { + if (idx >= num_args) { + continue; + } + + const zend_type *pre = (const zend_type *) Z_PTR_P(zv); + bool intersect; + if (!zend_generic_merge_polarity(defining_ce, pre, /* is_return_slot */ false, &intersect)) { + continue; + } + + zend_type sub = zend_substitute_leaf_type_param(*pre, binding->args, binding->count); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + continue; + } + + uint32_t slot = return_slot_offset + idx; + if (zend_diamond_types_equal(new_block[slot].type, sub)) { + continue; + } + + if (intersect && (!zend_type_intersectable(new_block[slot].type) || !zend_type_intersectable(sub))) { + zend_diamond_uninhabited_intersection_error( + ce, defining_ce, existing, + /* is_return_slot */ false, (uint32_t) idx, + new_block[slot].type, sub); + } + + new_block[slot].type = zend_synth_variance_merged_type(new_block[slot].type, sub, intersect); + } ZEND_HASH_FOREACH_END(); + (void) idx; + } + + zend_function *merged_fn = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(merged_fn, existing, sizeof(zend_op_array)); + merged_fn->op_array.fn_flags &= ~ZEND_ACC_IMMUTABLE; + merged_fn->op_array.arg_info = has_return ? new_block + 1 : new_block; + ZEND_MAP_PTR_INIT(merged_fn->op_array.run_time_cache, NULL); + ZEND_MAP_PTR_INIT(merged_fn->op_array.static_variables_ptr, NULL); + zval *slot = zend_hash_find_known_hash(&ce->function_table, key); + ZEND_ASSERT(slot != NULL && Z_PTR_P(slot) == existing); + Z_PTR_P(slot) = merged_fn; +} + +static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_string *key, zend_function *fn, uint32_t trait_idx) /* {{{ */ +{ + zend_function *existing_fn = NULL; + zend_function *new_fn; + + if ((existing_fn = zend_hash_find_ptr(&ce->function_table, key)) != NULL) { + /* if it is the same function with the same visibility and has not been assigned a class scope yet, regardless + * of where it is coming from there is no conflict and we do not need to add it again */ + if (existing_fn->op_array.opcodes == fn->op_array.opcodes && + (existing_fn->common.fn_flags & ZEND_ACC_PPP_MASK) == (fn->common.fn_flags & ZEND_ACC_PPP_MASK) && (existing_fn->common.scope->ce_flags & ZEND_ACC_TRAIT)) { + const zend_type_named_with_args *binding = zend_get_trait_use_binding_by_index(ce, trait_idx); + zend_trait_diamond_merge_method(ce, key, existing_fn, fn, binding); return; } @@ -2406,6 +3672,28 @@ static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_ /* Reassign method name, in case it is an alias. */ new_fn->common.function_name = name; function_add_ref(new_fn); + + if (fn->type == ZEND_USER_FUNCTION && fn->common.scope && fn->common.scope->generic_parameters) { + const zend_type_named_with_args *binding = zend_find_trait_use_binding_by_name(ce, fn->common.scope->name); + uint32_t cap = fn->common.scope->generic_parameters->count; + ALLOCA_FLAG(use_heap) + zend_type *default_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + const zend_type *bind_args = NULL; + uint32_t bind_arity = 0; + if (binding) { + bind_args = binding->args; + bind_arity = binding->count; + } else if (zend_get_target_default_args(fn->common.scope, default_args, cap, &bind_arity)) { + bind_args = default_args; + } + + if (bind_args) { + zend_substitute_trait_method_arg_info(new_fn, fn, bind_args, bind_arity); + } + + free_alloca(default_args, use_heap); + } + fn = zend_hash_update_ptr(&ce->function_table, key, new_fn); zend_add_magic_method(ce, fn, key); } @@ -2440,7 +3728,7 @@ static void zend_traits_check_private_final_inheritance(uint32_t original_fn_fla } } -static void zend_traits_copy_functions(zend_string *fnname, zend_function *fn, zend_class_entry *ce, HashTable *exclude_table, zend_class_entry **aliases) /* {{{ */ +static void zend_traits_copy_functions(zend_string *fnname, zend_function *fn, zend_class_entry *ce, HashTable *exclude_table, zend_class_entry **aliases, uint32_t trait_idx) /* {{{ */ { zend_trait_alias *alias, **alias_ptr; zend_function fn_copy; @@ -2467,7 +3755,7 @@ static void zend_traits_copy_functions(zend_string *fnname, zend_function *fn, z zend_traits_check_private_final_inheritance(fn->common.fn_flags, &fn_copy, alias->alias); zend_string *lcname = zend_string_tolower(alias->alias); - zend_add_trait_method(ce, alias->alias, lcname, &fn_copy); + zend_add_trait_method(ce, alias->alias, lcname, &fn_copy, trait_idx); zend_string_release_ex(lcname, 0); } alias_ptr++; @@ -2505,7 +3793,7 @@ static void zend_traits_copy_functions(zend_string *fnname, zend_function *fn, z zend_traits_check_private_final_inheritance(fn->common.fn_flags, &fn_copy, fnname); - zend_add_trait_method(ce, fn->common.function_name, fnname, &fn_copy); + zend_add_trait_method(ce, fn->common.function_name, fnname, &fn_copy, trait_idx); } } /* }}} */ @@ -2695,7 +3983,7 @@ static void zend_do_traits_method_binding(zend_class_entry *ce, zend_class_entry if (verify_abstract != is_abstract) { continue; } - zend_traits_copy_functions(key, fn, ce, exclude_tables[i], aliases); + zend_traits_copy_functions(key, fn, ce, exclude_tables[i], aliases, i); } ZEND_HASH_FOREACH_END(); if (exclude_tables[i]) { @@ -2714,7 +4002,7 @@ static void zend_do_traits_method_binding(zend_class_entry *ce, zend_class_entry if (verify_abstract != is_abstract) { continue; } - zend_traits_copy_functions(key, fn, ce, NULL, aliases); + zend_traits_copy_functions(key, fn, ce, NULL, aliases, i); } ZEND_HASH_FOREACH_END(); } } @@ -2966,6 +4254,25 @@ static void zend_do_traits_property_binding(zend_class_entry *ce, zend_class_ent zend_string *doc_comment = property_info->doc_comment ? zend_string_copy(property_info->doc_comment) : NULL; zend_type type = property_info->type; + const zend_type *pre_erasure = zend_get_trait_property_pre_erasure(traits[i], prop_name); + if (pre_erasure && traits[i]->generic_parameters && traits[i]->generic_parameters->count > 0) { + const zend_type *bind_args; + uint32_t bind_arity; + uint32_t cap = traits[i]->generic_parameters->count; + ALLOCA_FLAG(use_heap) + zend_type *default_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + if (zend_get_trait_use_binding(ce, i, &bind_args, &bind_arity)) { + type = zend_substitute_leaf_type_param(*pre_erasure, bind_args, bind_arity); + } else if (zend_get_target_default_args(traits[i], default_args, cap, &bind_arity)) { + type = zend_substitute_leaf_type_param(*pre_erasure, default_args, bind_arity); + } + + free_alloca(default_args, use_heap); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + type = property_info->type; + } + } + /* Assumption: only userland classes can use traits, as such the type must be arena allocated */ zend_type_copy_ctor(&type, /* use arena */ true, /* persistent */ false); zend_property_info *new_prop = zend_declare_typed_property(ce, prop_name, prop_value, flags, doc_comment, type); @@ -3224,7 +4531,7 @@ static void add_property_hook_obligation( static void resolve_delayed_variance_obligations(zend_class_entry *ce); -static void check_variance_obligation(const variance_obligation *obligation) { +static void check_variance_obligation(zend_class_entry *ce, const variance_obligation *obligation) { if (obligation->type == OBLIGATION_DEPENDENCY) { zend_class_entry *dependency_ce = obligation->dependency_ce; if (dependency_ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE) { @@ -3238,7 +4545,7 @@ static void check_variance_obligation(const variance_obligation *obligation) { } else if (obligation->type == OBLIGATION_COMPATIBILITY) { inheritance_status status = zend_do_perform_implementation_check( &obligation->child_fn, obligation->child_scope, - &obligation->parent_fn, obligation->parent_scope); + &obligation->parent_fn, obligation->parent_scope, ce); if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { emit_incompatible_method_error( &obligation->child_fn, obligation->child_scope, @@ -3301,7 +4608,7 @@ static void resolve_delayed_variance_obligations(zend_class_entry *ce) { variance_obligation *obligation; ZEND_HASH_FOREACH_PTR(obligations, obligation) { - check_variance_obligation(obligation); + check_variance_obligation(ce, obligation); } ZEND_HASH_FOREACH_END(); zend_inheritance_check_override(ce); @@ -3482,6 +4789,739 @@ static zend_class_entry *zend_lazy_class_load(const zend_class_entry *pce) } while (0) #endif +static void zend_check_generic_link_arity( + const zend_class_entry *target_ce, + uint32_t arity, + const char *clause, + zend_string *child_name) +{ + uint32_t total = target_ce->generic_parameters ? target_ce->generic_parameters->count : 0; + uint32_t required = 0; + if (target_ce->generic_parameters) { + while (required < total + && !ZEND_TYPE_IS_SET(target_ce->generic_parameters->parameters[required].default_type)) { + required++; + } + } + + if (arity > total) { + zend_error_noreturn(E_COMPILE_ERROR, + "Too many generic type arguments to %s %s in %s, %u passed and %s %u expected", + clause, ZSTR_VAL(target_ce->name), ZSTR_VAL(child_name), arity, + required == total ? "exactly" : "at most", total); + } + + if (arity < required) { + zend_error_noreturn(E_COMPILE_ERROR, + "Too few generic type arguments to %s %s in %s, %u passed and %s %u expected", + clause, ZSTR_VAL(target_ce->name), ZSTR_VAL(child_name), arity, + required == total ? "exactly" : "at least", required); + } +} + +static const zend_type *zend_lookup_inheritance_args(const HashTable *side_table, zend_ulong idx) +{ + if (!side_table) { + return NULL; + } + + zval *zv = zend_hash_index_find(side_table, idx); + if (!zv) { + return NULL; + } + + const zend_type *boxed = (const zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + return NULL; + } + + return boxed; +} + +typedef enum _zend_variance_polarity { + ZEND_VAR_POL_COVARIANT, + ZEND_VAR_POL_CONTRAVARIANT, + ZEND_VAR_POL_INVARIANT, +} zend_variance_polarity; + +static zend_variance_polarity zend_variance_compose( + zend_variance_polarity outer, zend_variance_polarity slot) +{ + if (outer == ZEND_VAR_POL_INVARIANT || slot == ZEND_VAR_POL_INVARIANT) { + return ZEND_VAR_POL_INVARIANT; + } + return (outer == slot) ? ZEND_VAR_POL_COVARIANT : ZEND_VAR_POL_CONTRAVARIANT; +} + +static zend_variance_polarity zend_variance_polarity_from(zend_generic_variance v) +{ + switch (v) { + case ZEND_GENERIC_VARIANCE_COVARIANT: return ZEND_VAR_POL_COVARIANT; + case ZEND_GENERIC_VARIANCE_CONTRAVARIANT: return ZEND_VAR_POL_CONTRAVARIANT; + default: return ZEND_VAR_POL_INVARIANT; + } +} + +static const char *zend_variance_polarity_name(zend_variance_polarity p) +{ + switch (p) { + case ZEND_VAR_POL_COVARIANT: return "covariant"; + case ZEND_VAR_POL_CONTRAVARIANT: return "contravariant"; + default: return "invariant"; + } +} + +static const char *zend_variance_marker(zend_generic_variance v) +{ + switch (v) { + case ZEND_GENERIC_VARIANCE_COVARIANT: return "+"; + case ZEND_GENERIC_VARIANCE_CONTRAVARIANT: return "-"; + default: return ""; + } +} + +static bool zend_variance_compatible( + zend_generic_variance declared, zend_variance_polarity at) +{ + if (declared == ZEND_GENERIC_VARIANCE_INVARIANT) { + return true; + } + + if (at == ZEND_VAR_POL_INVARIANT) { + return false; + } + + return zend_variance_polarity_from(declared) == at; +} + +static void zend_variance_walk( + const zend_generic_parameter_list *class_params, + const zend_generic_parameter_list *func_params, + zend_type t, + zend_variance_polarity pol) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); + const zend_generic_parameter_list *params = + (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) ? class_params : func_params; + if (!params) { + return; + } + + ZEND_ASSERT(ref->index < params->count); + zend_generic_variance declared = params->parameters[ref->index].variance; + if (!zend_variance_compatible(declared, pol)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s declared %s (%sT) cannot appear in %s position", + ZSTR_VAL(ref->name), + zend_variance_polarity_name(zend_variance_polarity_from(declared)), + zend_variance_marker(declared), + zend_variance_polarity_name(pol)); + } + + return; + } + + if (ZEND_TYPE_HAS_LIST(t)) { + const zend_type_list *list = ZEND_TYPE_LIST(t); + for (uint32_t i = 0; i < list->num_types; i++) { + zend_variance_walk(class_params, func_params, list->types[i], pol); + } + + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(t)) { + const zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(t); + zend_class_entry *target = NULL; + if (named->name) { + zend_class_entry *active = CG(active_class_entry); + if (active) { + if (zend_string_equals_literal_ci(named->name, "self") + || zend_string_equals_literal_ci(named->name, "static") + || (active->name && zend_string_equals_ci(named->name, active->name))) { + target = active; + } else if (zend_string_equals_literal_ci(named->name, "parent")) { + if (active->ce_flags & ZEND_ACC_RESOLVED_PARENT) { + target = active->parent; + } else if (active->parent_name) { + target = zend_lookup_class_ex(active->parent_name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + } + } + } + + if (!target) { + target = zend_lookup_class_ex(named->name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + } + } + + if (!target || !target->generic_parameters) { + return; + } + + for (uint32_t i = 0; i < named->count; i++) { + zend_variance_polarity slot = i < target->generic_parameters->count + ? zend_variance_polarity_from(target->generic_parameters->parameters[i].variance) + : ZEND_VAR_POL_INVARIANT; + + zend_variance_walk(class_params, func_params, named->args[i], zend_variance_compose(pol, slot)); + } + + return; + } +} + +static void zend_variance_walk_function( + const zend_generic_parameter_list *class_params, + const zend_generic_parameter_list *func_params, + const zend_op_array *op_array) +{ + if (!op_array->generic_types && !op_array->generic_parameters) { + return; + } + + if (op_array->generic_types) { + if (op_array->generic_types->parameters) { + zval *zv; + zend_ulong h; + ZEND_HASH_FOREACH_NUM_KEY_VAL(op_array->generic_types->parameters, h, zv) { + const zend_type *t = (const zend_type *) Z_PTR_P(zv); + zend_variance_walk(class_params, func_params, *t, ZEND_VAR_POL_CONTRAVARIANT); + } ZEND_HASH_FOREACH_END(); + (void) h; + } + + if (op_array->generic_types->return_type) { + zend_variance_walk(class_params, func_params, *op_array->generic_types->return_type, ZEND_VAR_POL_COVARIANT); + } + } + + if (op_array->generic_parameters) { + for (uint32_t i = 0; i < op_array->generic_parameters->count; i++) { + const zend_generic_parameter *p = &op_array->generic_parameters->parameters[i]; + if (ZEND_TYPE_IS_SET(p->bound_pre_erasure)) { + zend_variance_walk(class_params, func_params, p->bound_pre_erasure, ZEND_VAR_POL_INVARIANT); + } + + if (ZEND_TYPE_IS_SET(p->default_pre_erasure)) { + zend_variance_walk(class_params, func_params, p->default_pre_erasure, ZEND_VAR_POL_INVARIANT); + } + } + } +} + +static zend_variance_polarity zend_variance_polarity_for_property(const zend_property_info *prop) +{ + if (prop->hooks) { + const zend_function *get = prop->hooks[ZEND_PROPERTY_HOOK_GET]; + const zend_function *set = prop->hooks[ZEND_PROPERTY_HOOK_SET]; + bool by_ref_get = get && (get->common.fn_flags & ZEND_ACC_RETURN_REFERENCE); + if (by_ref_get || (get && set)) { + return ZEND_VAR_POL_INVARIANT; + } + + if (get) { + return ZEND_VAR_POL_COVARIANT; + } + + if (set) { + return ZEND_VAR_POL_CONTRAVARIANT; + } + + return ZEND_VAR_POL_INVARIANT; + } + + if (prop->flags & ZEND_ACC_READONLY) { + return ZEND_VAR_POL_COVARIANT; + } + + return ZEND_VAR_POL_INVARIANT; +} + +void zend_check_generic_variance_markers(zend_class_entry *ce) +{ + if (!ce->generic_parameters) { + return; + } + + bool any_marked = false; + for (uint32_t i = 0; i < ce->generic_parameters->count; i++) { + if (ce->generic_parameters->parameters[i].variance != ZEND_GENERIC_VARIANCE_INVARIANT) { + any_marked = true; + break; + } + } + + if (!any_marked) { + return; + } + + const zend_generic_parameter_list *class_params = ce->generic_parameters; + uint32_t orig_lineno = CG(zend_lineno); + + for (uint32_t i = 0; i < class_params->count; i++) { + const zend_generic_parameter *p = &class_params->parameters[i]; + if (ZEND_TYPE_IS_SET(p->bound_pre_erasure)) { + zend_variance_walk(class_params, NULL, p->bound_pre_erasure, ZEND_VAR_POL_INVARIANT); + } + + if (ZEND_TYPE_IS_SET(p->default_pre_erasure)) { + zend_variance_walk(class_params, NULL, p->default_pre_erasure, ZEND_VAR_POL_INVARIANT); + } + } + + zend_function *fn; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) { + if (fn->common.fn_flags & (ZEND_ACC_STATIC | ZEND_ACC_CTOR)) { + continue; + } + + if (fn->common.scope != ce) { + continue; + } + + if (!ZEND_USER_CODE(fn->common.type)) { + continue; + } + + CG(zend_lineno) = fn->op_array.line_start; + zend_variance_walk_function(class_params, NULL, &fn->op_array); + } ZEND_HASH_FOREACH_END(); + CG(zend_lineno) = orig_lineno; + + if (ce->generic_types && ce->generic_types->properties) { + zend_property_info *prop_info; + ZEND_HASH_FOREACH_PTR(&ce->properties_info, prop_info) { + if (prop_info->ce != ce) { + continue; + } + + zend_variance_polarity prop_pol = zend_variance_polarity_for_property(prop_info); + zval *pre_zv = zend_hash_find(ce->generic_types->properties, prop_info->name); + if (pre_zv) { + const zend_type *pre = (const zend_type *) Z_PTR_P(pre_zv); + zend_variance_walk(class_params, NULL, *pre, prop_pol); + } + + if (prop_info->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + zend_function *hook = prop_info->hooks[i]; + if (hook && ZEND_USER_CODE(hook->common.type)) { + CG(zend_lineno) = hook->op_array.line_start; + zend_variance_walk_function(class_params, NULL, &hook->op_array); + } + } + + CG(zend_lineno) = orig_lineno; + } + } ZEND_HASH_FOREACH_END(); + } + + if (ce->generic_types) { + if (ce->generic_types->extends) { + zend_variance_walk(class_params, NULL, *ce->generic_types->extends, ZEND_VAR_POL_COVARIANT); + } + + if (ce->generic_types->implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) { + zend_variance_walk(class_params, NULL, *(const zend_type *) Z_PTR_P(zv), ZEND_VAR_POL_COVARIANT); + } ZEND_HASH_FOREACH_END(); + } + + if (ce->generic_types->trait_uses) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->trait_uses, zv) { + zend_variance_walk(class_params, NULL, *(const zend_type *) Z_PTR_P(zv), ZEND_VAR_POL_COVARIANT); + } ZEND_HASH_FOREACH_END(); + } + } +} + +void zend_check_function_variance_markers(zend_op_array *op_array) +{ + if (!op_array->generic_parameters) { + return; + } + + bool any_marked = false; + for (uint32_t i = 0; i < op_array->generic_parameters->count; i++) { + if (op_array->generic_parameters->parameters[i].variance != ZEND_GENERIC_VARIANCE_INVARIANT) { + any_marked = true; + break; + } + } + + if (!any_marked) { + return; + } + + zend_variance_walk_function(NULL, op_array->generic_parameters, op_array); +} + +static void zend_check_generic_link_bounds( + zend_class_entry *target_ce, + const zend_type *args_box, + const char *clause, + zend_class_entry *ce) +{ + if (!args_box || !target_ce->generic_parameters) { + return; + } + + const zend_type_named_with_args *args = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + uint32_t check_count = args->count; + if (check_count > target_ce->generic_parameters->count) { + check_count = target_ce->generic_parameters->count; + } + + for (uint32_t i = 0; i < check_count; i++) { + zend_type bound = target_ce->generic_parameters->parameters[i].bound; + if (!ZEND_TYPE_IS_SET(bound)) { + continue; + } + + zend_type arg = args->args[i]; + zend_type effective = arg; + bool ce_bound_unset = false; + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(arg)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(arg); + if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE + && ce->generic_parameters + && ref->index < ce->generic_parameters->count) { + zend_type ce_bound = ce->generic_parameters->parameters[ref->index].bound; + if (ZEND_TYPE_IS_SET(ce_bound)) { + effective = ce_bound; + } else { + ce_bound_unset = true; + } + } + } + + if (ce_bound_unset || zend_check_generic_arg_satisfies_bound(ce, effective, target_ce, bound) != INHERITANCE_SUCCESS) { + zend_type bound_display = ZEND_TYPE_IS_SET(target_ce->generic_parameters->parameters[i].bound_pre_erasure) + ? target_ce->generic_parameters->parameters[i].bound_pre_erasure + : bound; + zend_string *bound_str = zend_type_to_string(bound_display); + zend_string *arg_str; + if (ZEND_TYPE_HAS_TYPE_PARAMETER(arg)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(arg); + arg_str = ref->name ? zend_string_copy(ref->name) : zend_string_init("T", 1, 0); + } else { + arg_str = zend_type_to_string(arg); + } + + const char *param_name = + target_ce->generic_parameters->parameters[i].name + ? ZSTR_VAL(target_ce->generic_parameters->parameters[i].name) + : "?"; + zend_error_noreturn(E_COMPILE_ERROR, + "Type argument %u to %s %s in %s does not satisfy the bound %s on parameter %s, %s given", + i + 1, clause, ZSTR_VAL(target_ce->name), ZSTR_VAL(ce->name), + ZSTR_VAL(bound_str), param_name, ZSTR_VAL(arg_str)); + zend_string_release(bound_str); + zend_string_release(arg_str); + } + } +} + +static uint32_t zend_lookup_inheritance_arity(const HashTable *side_table, zend_ulong idx) +{ + if (!side_table) { + return 0; + } + + zval *zv = zend_hash_index_find(side_table, idx); + if (!zv) { + return 0; + } + + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + return 0; + } + + return ZEND_TYPE_NAMED_WITH_ARGS(*boxed)->count; +} + +static bool zend_diamond_types_equal(zend_type a, zend_type b) +{ + if (ZEND_TYPE_PURE_MASK(a) != ZEND_TYPE_PURE_MASK(b)) { + return false; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(a)) { + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(b)) { + return false; + } + const zend_type_named_with_args *na = ZEND_TYPE_NAMED_WITH_ARGS(a); + const zend_type_named_with_args *nb = ZEND_TYPE_NAMED_WITH_ARGS(b); + if (na->count != nb->count) { + return false; + } + + if ((na->name == NULL) != (nb->name == NULL)) { + return false; + } + + if (na->name && !zend_string_equals_ci(na->name, nb->name)) { + return false; + } + + for (uint32_t i = 0; i < na->count; i++) { + if (!zend_diamond_types_equal(na->args[i], nb->args[i])) { + return false; + } + } + + return true; + } + + if (ZEND_TYPE_HAS_LIST(a)) { + if (!ZEND_TYPE_HAS_LIST(b)) { + return false; + } + + const zend_type_list *la = ZEND_TYPE_LIST(a); + const zend_type_list *lb = ZEND_TYPE_LIST(b); + if (la->num_types != lb->num_types) { + return false; + } + + for (uint32_t i = 0; i < la->num_types; i++) { + if (!zend_diamond_types_equal(la->types[i], lb->types[i])) { + return false; + } + } + + return true; + } + + if (ZEND_TYPE_HAS_NAME(a)) { + if (!ZEND_TYPE_HAS_NAME(b)) { + return false; + } + + return zend_string_equals_ci(ZEND_TYPE_NAME(a), ZEND_TYPE_NAME(b)); + } + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(a)) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(b)) { + return false; + } + + const zend_type_parameter_ref *ra = ZEND_TYPE_TYPE_PARAMETER(a); + const zend_type_parameter_ref *rb = ZEND_TYPE_TYPE_PARAMETER(b); + return ra->origin == rb->origin && ra->index == rb->index; + } + + return true; +} + +typedef struct { + uint32_t arity; + zend_string *first_source_name; + zend_type args[1]; +} zend_diamond_record; + +#define ZEND_DIAMOND_RECORD_SIZE(arity) \ + (offsetof(zend_diamond_record, args) + sizeof(zend_type) * (arity)) + +static zend_string *zend_diamond_format_args(const zend_type *args, uint32_t arity) +{ + smart_str buf = {0}; + smart_str_appendc(&buf, '<'); + for (uint32_t j = 0; j < arity; j++) { + if (j > 0) smart_str_appends(&buf, ", "); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(args[j])) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(args[j]); + smart_str_append(&buf, ref->name ? ref->name : ZSTR_KNOWN(ZEND_STR_MIXED)); + } else { + zend_string *piece = zend_type_to_string(args[j]); + if (piece) { + smart_str_append(&buf, piece); + zend_string_release(piece); + } else { + smart_str_appendc(&buf, '?'); + } + } + } + + smart_str_appendc(&buf, '>'); + smart_str_0(&buf); + return buf.s; +} + +static void zend_diamond_record_dtor(zval *zv) +{ + efree(Z_PTR_P(zv)); +} + +static void zend_diamond_record_or_check( + zend_class_entry *ce, + zend_class_entry *target, + const zend_type *args, uint32_t arity, + zend_string *source_name, + HashTable *records) +{ + zend_ulong key = (zend_ulong)(uintptr_t) target; + zend_diamond_record *prior = zend_hash_index_find_ptr(records, key); + if (prior) { + if (prior->arity == arity) { + return; + } + + zend_string *first_args = zend_diamond_format_args(prior->args, prior->arity); + zend_string *next_args = zend_diamond_format_args(args, arity); + zend_error_noreturn(E_COMPILE_ERROR, + "%s inherits %s%s via %s and %s%s via %s", + ZSTR_VAL(ce->name), + ZSTR_VAL(target->name), ZSTR_VAL(first_args), ZSTR_VAL(prior->first_source_name), + ZSTR_VAL(target->name), ZSTR_VAL(next_args), ZSTR_VAL(source_name)); + } + + zend_diamond_record *record = emalloc(ZEND_DIAMOND_RECORD_SIZE(arity)); + record->arity = arity; + record->first_source_name = source_name; + for (uint32_t j = 0; j < arity; j++) { + record->args[j] = args[j]; + } + + zend_hash_index_add_new_ptr(records, key, record); +} + +static const zend_type_named_with_args *zend_get_extends_binding(const zend_class_entry *ce) +{ + if (!ce->generic_types || !ce->generic_types->extends) return NULL; + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) return NULL; + return ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); +} + +static const zend_type_named_with_args *zend_get_implements_binding(const zend_class_entry *ce, uint32_t idx) +{ + if (!ce->generic_types || !ce->generic_types->implements) return NULL; + zval *zv = zend_hash_index_find(ce->generic_types->implements, idx); + if (!zv) return NULL; + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) return NULL; + return ZEND_TYPE_NAMED_WITH_ARGS(*boxed); +} + +static void zend_diamond_collect_via_provider( + zend_class_entry *ce, + zend_class_entry *provider, + const zend_type *ce_to_provider, + uint32_t ce_to_provider_arity, + zend_string *source_name, + HashTable *records) +{ + if (!provider) return; + + if (provider->generic_parameters && ce_to_provider) { + zend_diamond_record_or_check(ce, provider, ce_to_provider, ce_to_provider_arity, + source_name, records); + } + + for (uint32_t i = 0; i < provider->num_interfaces; i++) { + zend_class_entry *target = provider->interfaces[i]; + if (!target || !target->generic_parameters) continue; + + uint32_t cap = target->generic_parameters->count; + if (cap == 0) continue; + ALLOCA_FLAG(use_heap) + zend_type *via = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t via_arity; + if (!zend_get_inheritance_binding_full_cached(provider, target, via, cap, &via_arity)) { + free_alloca(via, use_heap); + continue; + } + + if (ce_to_provider) { + for (uint32_t j = 0; j < via_arity; j++) { + via[j] = zend_substitute_leaf_type_param(via[j], ce_to_provider, ce_to_provider_arity); + } + } + + zend_diamond_record_or_check(ce, target, via, via_arity, + source_name, records); + free_alloca(via, use_heap); + } +} + +static void zend_validate_generic_diamond_bindings( + zend_class_entry *ce, + zend_class_entry *parent_ce, + zend_class_entry **traits_and_interfaces) +{ + if (!parent_ce && (!traits_and_interfaces || ce->num_interfaces == 0)) return; + + HashTable records; + zend_hash_init(&records, 8, NULL, zend_diamond_record_dtor, /* persistent */ 0); + + if (parent_ce) { + const zend_type_named_with_args *binding = zend_get_extends_binding(ce); + zend_diamond_collect_via_provider(ce, parent_ce, + binding ? binding->args : NULL, + binding ? binding->count : 0, + parent_ce->name, &records); + } + + if (traits_and_interfaces) { + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + zend_class_entry *DI = traits_and_interfaces[ce->num_traits + i]; + if (!DI) continue; + const zend_type_named_with_args *binding = zend_get_implements_binding(ce, i); + zend_diamond_collect_via_provider(ce, DI, + binding ? binding->args : NULL, + binding ? binding->count : 0, + DI->name, &records); + } + } + + zend_hash_destroy(&records); +} + +static void zend_validate_generic_inheritance_arities( + zend_class_entry *ce, + zend_class_entry *parent_ce, + zend_class_entry **traits_and_interfaces) +{ + if (!traits_and_interfaces && !parent_ce) { + return; + } + + zend_validate_generic_diamond_bindings(ce, parent_ce, traits_and_interfaces); + if (!traits_and_interfaces) { + return; + } + + const HashTable *trait_uses_table = ce->generic_types ? ce->generic_types->trait_uses : NULL; + for (uint32_t i = 0; i < ce->num_traits; i++) { + zend_class_entry *trait_ce = traits_and_interfaces[i]; + if (!trait_ce) continue; + uint32_t arity = zend_lookup_inheritance_arity(trait_uses_table, i); + if (arity > 0 || trait_ce->generic_parameters) { + zend_check_generic_link_arity(trait_ce, arity, "use", ce->name); + } + + zend_check_generic_link_bounds(trait_ce, zend_lookup_inheritance_args(trait_uses_table, i), "use", ce); + } + + const HashTable *implements_table = ce->generic_types ? ce->generic_types->implements : NULL; + const char *clause = (ce->ce_flags & ZEND_ACC_INTERFACE) ? "extends" : "implements"; + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + zend_class_entry *iface_ce = traits_and_interfaces[ce->num_traits + i]; + if (!iface_ce) continue; + uint32_t arity = zend_lookup_inheritance_arity(implements_table, i); + if (arity > 0 || iface_ce->generic_parameters) { + zend_check_generic_link_arity(iface_ce, arity, clause, ce->name); + } + + zend_check_generic_link_bounds(iface_ce, zend_lookup_inheritance_args(implements_table, i), clause, ce); + } +} + ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name, const zend_string *key) /* {{{ */ { /* Load parent/interface dependencies first, so we can still gracefully abort linking @@ -3534,8 +5574,22 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } for (j = 0; j < i; j++) { if (traits_and_interfaces[j] == trait) { - /* skip duplications */ - trait = NULL; + bool keep_for_diamond = false; + if (trait->generic_parameters) { + const zend_type_named_with_args *prior = zend_get_trait_use_binding_by_index(ce, j); + const zend_type_named_with_args *cur = zend_get_trait_use_binding_by_index(ce, i); + if (prior && cur + && zend_iface_diamond_bindings_allowed( + trait, prior->args, prior->count, + cur->args, cur->count)) { + keep_for_diamond = true; + } + } + + if (!keep_for_diamond) { + trait = NULL; + } + break; } } @@ -3564,6 +5618,13 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } } + zend_validate_generic_inheritance_arities(ce, parent, traits_and_interfaces); + + zend_class_entry *orig_active = CG(active_class_entry); + CG(active_class_entry) = ce; + zend_check_generic_variance_markers(ce); + CG(active_class_entry) = orig_active; + #ifndef ZEND_WIN32 if (ce->ce_flags & ZEND_ACC_ENUM) { /* We will add internal methods. */ @@ -4027,3 +6088,14 @@ ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_ return NULL; } /* }}} */ + +ZEND_API zend_inheritance_status zend_check_generic_arg_satisfies_bound( + zend_class_entry *arg_scope, zend_type arg, + zend_class_entry *bound_scope, zend_type bound) +{ + if (!ZEND_TYPE_IS_SET(bound)) { + return INHERITANCE_SUCCESS; + } + + return zend_perform_covariant_type_check(arg_scope, arg, bound_scope, bound); +} diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index fdcbd95764b3..5bd085e90cea 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -24,8 +24,33 @@ BEGIN_EXTERN_C() +typedef enum { + INHERITANCE_UNRESOLVED = -1, + INHERITANCE_ERROR = 0, + INHERITANCE_WARNING = 1, + INHERITANCE_SUCCESS = 2, +} zend_inheritance_status; + +typedef struct _zend_inheritance_binding_cache { + const zend_class_entry *ce; + const zend_class_entry *target; + uint32_t arity; + bool present; + bool valid; + zend_type args[ZEND_GENERIC_MAX_PARAMS]; +} zend_inheritance_binding_cache; + ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface); ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *parent_ce, bool checked); +ZEND_API zend_inheritance_status zend_check_generic_arg_satisfies_bound( + zend_class_entry *arg_scope, zend_type arg, + zend_class_entry *bound_scope, zend_type bound); +ZEND_API bool zend_get_inheritance_binding_full( + const zend_class_entry *ce, + const zend_class_entry *target, + zend_type *out_args, + uint32_t out_capacity, + uint32_t *out_arity); static zend_always_inline void zend_do_inheritance(zend_class_entry *ce, zend_class_entry *parent_ce) { zend_do_inheritance_ex(ce, parent_ce, 0); @@ -38,17 +63,12 @@ void zend_build_properties_info_table(zend_class_entry *ce); ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_entry *parent_ce, zend_string *lcname, zval *delayed_early_binding); void zend_inheritance_check_override(const zend_class_entry *ce); +void zend_check_generic_variance_markers(zend_class_entry *ce); +void zend_check_function_variance_markers(zend_op_array *op_array); ZEND_API extern zend_class_entry* (*zend_inheritance_cache_get)(zend_class_entry *ce, zend_class_entry *parent, zend_class_entry **traits_and_interfaces); ZEND_API extern zend_class_entry* (*zend_inheritance_cache_add)(zend_class_entry *ce, zend_class_entry *proto, zend_class_entry *parent, zend_class_entry **traits_and_interfaces, HashTable *dependencies); -typedef enum { - INHERITANCE_UNRESOLVED = -1, - INHERITANCE_ERROR = 0, - INHERITANCE_WARNING = 1, - INHERITANCE_SUCCESS = 2, -} zend_inheritance_status; - ZEND_API zend_inheritance_status zend_verify_property_hook_variance(const zend_property_info *prop_info, const zend_function *func); ZEND_API ZEND_COLD ZEND_NORETURN void zend_hooked_property_variance_error(const zend_property_info *prop_info); ZEND_API ZEND_COLD ZEND_NORETURN void zend_hooked_property_variance_error_ex(zend_string *value_param_name, zend_string *class_name, zend_string *prop_name); diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index b4dda00404ea..96bf9f015e5b 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -50,6 +50,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %destructor { zend_ast_destroy($$); } %destructor { if ($$) zend_string_release_ex($$, 0); } +%precedence PREC_NO_TYPE_ARGS %precedence T_THROW %precedence PREC_ARROW_FUNCTION %precedence T_INCLUDE T_INCLUDE_ONCE T_REQUIRE T_REQUIRE_ONCE @@ -232,6 +233,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_DOLLAR_OPEN_CURLY_BRACES "'${'" %token T_CURLY_OPEN "'{$'" %token T_PAAMAYIM_NEKUDOTAYIM "'::'" +%token T_TURBOFISH "'::<'" %token T_NS_SEPARATOR "'\\'" %token T_ELLIPSIS "'...'" %token T_COALESCE "'??'" @@ -288,6 +290,16 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body %type optional_parameter_list clone_argument_list non_empty_clone_argument_list +%type optional_generic_type_parameter_list generic_type_parameter_list +%type generic_type_parameter_list_inner generic_type_parameter +%type optional_generic_type_parameter_bound optional_generic_type_parameter_default +%type optional_generic_type_argument_list generic_type_argument_list +%type generic_type_argument_list_inner +%type optional_call_type_argument_list call_type_argument_list call_type_argument_list_inner +%type bound_class_name bound_class_name_reference + +%type optional_generic_variance + %type returns_ref function fn is_reference is_variadic property_modifiers property_hook_modifiers %type method_modifiers class_const_modifiers member_modifier optional_cpp_modifiers %type class_modifiers class_modifier anonymous_class_modifiers anonymous_class_modifiers_optional use_type backup_fn_flags @@ -364,10 +376,18 @@ name: ; attribute_decl: - class_name - { $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, NULL); } - | class_name argument_list - { $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, $2); } + class_name optional_call_type_argument_list + { + uint32_t _ga = $2 ? zend_ast_get_list($2)->children : 0; + $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, NULL, $2); + $$->attr = (zend_ast_attr) _ga; + } + | class_name optional_call_type_argument_list argument_list + { + uint32_t _ga = $2 ? zend_ast_get_list($2)->children : 0; + $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, $3, $2); + $$->attr = (zend_ast_attr) _ga; + } ; attribute_group: @@ -551,8 +571,8 @@ catch_list: ; catch_name_list: - class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } - | catch_name_list '|' class_name { $$ = zend_ast_list_add($1, $3); } + bound_class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } + | catch_name_list '|' bound_class_name { $$ = zend_ast_list_add($1, $3); } ; optional_variable: @@ -584,10 +604,10 @@ function_name: ; function_declaration_statement: - function returns_ref function_name backup_doc_comment '(' parameter_list ')' return_type + function returns_ref function_name optional_generic_type_parameter_list backup_doc_comment '(' parameter_list ')' return_type backup_fn_flags '{' inner_statement_list '}' backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2 | $13, $1, $4, - zend_ast_get_str($3), $6, NULL, $11, $8, NULL); CG(extra_fn_flags) = $9; } + { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2 | $14, $1, $5, + zend_ast_get_str($3), $7, NULL, $12, $9, NULL, $4); CG(extra_fn_flags) = $10; } ; is_reference: @@ -602,11 +622,11 @@ is_variadic: class_declaration_statement: class_modifiers T_CLASS { $$ = CG(zend_lineno); } - T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $3, $7, zend_ast_get_str($4), $5, $6, $9, NULL, NULL); } + T_STRING optional_generic_type_parameter_list extends_from implements_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $3, $8, zend_ast_get_str($4), $6, $7, $10, NULL, NULL, $5); } | T_CLASS { $$ = CG(zend_lineno); } - T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $2, $6, zend_ast_get_str($3), $4, $5, $8, NULL, NULL); } + T_STRING optional_generic_type_parameter_list extends_from implements_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $2, $7, zend_ast_get_str($3), $5, $6, $9, NULL, NULL, $4); } ; class_modifiers: @@ -635,20 +655,20 @@ class_modifier: trait_declaration_statement: T_TRAIT { $$ = CG(zend_lineno); } - T_STRING backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_TRAIT, $2, $4, zend_ast_get_str($3), NULL, NULL, $6, NULL, NULL); } + T_STRING optional_generic_type_parameter_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_TRAIT, $2, $5, zend_ast_get_str($3), NULL, NULL, $7, NULL, NULL, $4); } ; interface_declaration_statement: T_INTERFACE { $$ = CG(zend_lineno); } - T_STRING interface_extends_list backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_INTERFACE, $2, $5, zend_ast_get_str($3), NULL, $4, $7, NULL, NULL); } + T_STRING optional_generic_type_parameter_list interface_extends_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_INTERFACE, $2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, NULL, $4); } ; enum_declaration_statement: T_ENUM { $$ = CG(zend_lineno); } T_STRING enum_backing_type implements_list backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_ENUM|ZEND_ACC_FINAL, $2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, $4); } + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_ENUM|ZEND_ACC_FINAL, $2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, $4, NULL); } ; enum_backing_type: @@ -668,7 +688,7 @@ enum_case_expr: extends_from: %empty { $$ = NULL; } - | T_EXTENDS class_name { $$ = $2; } + | T_EXTENDS bound_class_name { $$ = $2; } ; interface_extends_list: @@ -830,6 +850,94 @@ optional_type_without_static: | type_expr_without_static { $$ = $1; } ; +optional_generic_type_parameter_list: + %empty { $$ = NULL; } + | generic_type_parameter_list { $$ = $1; } +; + +generic_type_parameter_list: + '<' { CG(type_arg_depth)++; } + generic_type_parameter_list_inner possible_comma '>' + { CG(type_arg_depth)--; zend_check_generic_param_list_size($3); $$ = $3; } +; + +generic_type_parameter_list_inner: + generic_type_parameter + { $$ = zend_ast_create_list(1, ZEND_AST_GENERIC_TYPE_PARAMETER_LIST, $1); } + | generic_type_parameter_list_inner ',' generic_type_parameter + { $$ = zend_ast_list_add($1, $3); } +; + +generic_type_parameter: + optional_generic_variance T_STRING optional_generic_type_parameter_bound + optional_generic_type_parameter_default + { $$ = zend_ast_create_ex(ZEND_AST_GENERIC_TYPE_PARAMETER, $1, $2, $3, $4); } +; + +optional_generic_variance: + %empty { $$ = 0; } + | '+' { $$ = 1; } + | '-' { $$ = 2; } +; + +optional_generic_type_parameter_bound: + %empty { $$ = NULL; } + | ':' type_expr { $$ = $2; } +; + +optional_generic_type_parameter_default: + %empty { $$ = NULL; } + | '=' type_expr { $$ = $2; } +; + +optional_generic_type_argument_list: + %empty %prec PREC_NO_TYPE_ARGS { $$ = NULL; } + | generic_type_argument_list { $$ = $1; } +; + +generic_type_argument_list: + '<' { CG(type_arg_depth)++; } + generic_type_argument_list_inner possible_comma '>' + { CG(type_arg_depth)--; zend_check_generic_arg_list_size($3); $$ = $3; } +; + +generic_type_argument_list_inner: + type_expr + { $$ = zend_ast_create_list(1, ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST, $1); } + | generic_type_argument_list_inner ',' type_expr + { $$ = zend_ast_list_add($1, $3); } +; + +optional_call_type_argument_list: + %empty { $$ = NULL; } + | call_type_argument_list { $$ = $1; } +; + +call_type_argument_list: + T_TURBOFISH { CG(type_arg_depth)++; } + call_type_argument_list_inner possible_comma '>' + { CG(type_arg_depth)--; zend_check_generic_arg_list_size($3); $$ = $3; } +; + +call_type_argument_list_inner: + type_expr + { $$ = zend_ast_create_list(1, ZEND_AST_GENERIC_CALL_TYPE_ARGUMENT_LIST, $1); } + | call_type_argument_list_inner ',' type_expr + { $$ = zend_ast_list_add($1, $3); } +; + +bound_class_name: + class_name optional_generic_type_argument_list + { $$ = $2 ? zend_ast_create(ZEND_AST_GENERIC_NAMED_TYPE, $1, $2) : $1; } +; + +bound_class_name_reference: + class_name optional_generic_type_argument_list + { $$ = $2 ? zend_ast_create(ZEND_AST_GENERIC_NAMED_TYPE, $1, $2) : $1; } + | new_variable { $$ = $1; } + | '(' expr ')' { $$ = $2; } +; + type_expr: type { $$ = $1; } | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } @@ -839,7 +947,9 @@ type_expr: type: type_without_static { $$ = $1; } - | T_STATIC { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC); } + | T_STATIC optional_generic_type_argument_list + { zend_ast *bare = zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC); + $$ = $2 ? zend_ast_create(ZEND_AST_GENERIC_NAMED_TYPE, bare, $2) : bare; } ; union_type_element: @@ -872,7 +982,8 @@ type_expr_without_static: type_without_static: T_ARRAY { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY); } | T_CALLABLE { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE); } - | name { $$ = $1; } + | name optional_generic_type_argument_list + { $$ = $2 ? zend_ast_create(ZEND_AST_GENERIC_NAMED_TYPE, $1, $2) : $1; } ; union_type_without_static_element: @@ -995,10 +1106,10 @@ attributed_class_statement: | class_const_modifiers T_CONST type_expr class_const_list ';' { $$ = zend_ast_create(ZEND_AST_CLASS_CONST_GROUP, $4, NULL, $3); $$->attr = $1; } - | method_modifiers function returns_ref identifier backup_doc_comment '(' parameter_list ')' + | method_modifiers function returns_ref identifier optional_generic_type_parameter_list backup_doc_comment '(' parameter_list ')' return_type backup_fn_flags method_body backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1 | $12, $2, $5, - zend_ast_get_str($4), $7, NULL, $11, $9, NULL); CG(extra_fn_flags) = $10; } + { $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1 | $13, $2, $6, + zend_ast_get_str($4), $8, NULL, $12, $10, NULL, $5); CG(extra_fn_flags) = $11; } | enum_case { $$ = $1; } ; @@ -1010,8 +1121,8 @@ class_statement: ; class_name_list: - class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } - | class_name_list ',' class_name { $$ = zend_ast_list_add($1, $3); } + bound_class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } + | class_name_list ',' bound_class_name { $$ = zend_ast_list_add($1, $3); } ; trait_adaptations: @@ -1164,7 +1275,7 @@ property_hook: optional_parameter_list backup_fn_flags property_hook_body backup_fn_flags { $$ = zend_ast_create_decl( ZEND_AST_PROPERTY_HOOK, $1 | $2 | $9, $5, $4, zend_ast_get_str($3), - $6, NULL, $8, NULL, NULL); + $6, NULL, $8, NULL, NULL, NULL); CG(extra_fn_flags) = $7; } ; @@ -1230,14 +1341,14 @@ anonymous_class: extends_from implements_list backup_doc_comment '{' class_statement_list '}' { zend_ast *decl = zend_ast_create_decl( ZEND_AST_CLASS, ZEND_ACC_ANON_CLASS | $1, $3, $7, NULL, - $5, $6, $9, NULL, NULL); - $$ = zend_ast_create(ZEND_AST_NEW, decl, $4); + $5, $6, $9, NULL, NULL, NULL); + $$ = zend_ast_create(ZEND_AST_NEW, decl, $4, NULL); } ; new_dereferenceable: - T_NEW class_name_reference argument_list - { $$ = zend_ast_create(ZEND_AST_NEW, $2, $3); } + T_NEW class_name_reference optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_NEW, $2, $4, $3); } | T_NEW anonymous_class { $$ = $2; } | T_NEW attributes anonymous_class @@ -1245,8 +1356,8 @@ new_dereferenceable: ; new_non_dereferenceable: - T_NEW class_name_reference - { $$ = zend_ast_create(ZEND_AST_NEW, $2, zend_ast_create_list(0, ZEND_AST_ARG_LIST)); } + T_NEW class_name_reference optional_call_type_argument_list + { $$ = zend_ast_create(ZEND_AST_NEW, $2, zend_ast_create_list(0, ZEND_AST_ARG_LIST), $3); } ; expr: @@ -1263,12 +1374,12 @@ expr: | T_CLONE clone_argument_list { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); name->attr = ZEND_NAME_FQ; - $$ = zend_ast_create(ZEND_AST_CALL, name, $2); + $$ = zend_ast_create(ZEND_AST_CALL, name, $2, NULL); } | T_CLONE expr { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); name->attr = ZEND_NAME_FQ; - $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $2)); + $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $2), NULL); } | variable T_PLUS_EQUAL expr { $$ = zend_ast_create_assign_op(ZEND_ADD, $1, $3); } @@ -1347,7 +1458,7 @@ expr: { $$ = zend_ast_create(ZEND_AST_GREATER_EQUAL, $1, $3); } | expr T_SPACESHIP expr { $$ = zend_ast_create_binary_op(ZEND_SPACESHIP, $1, $3); } - | expr T_INSTANCEOF class_name_reference + | expr T_INSTANCEOF bound_class_name_reference { $$ = zend_ast_create(ZEND_AST_INSTANCEOF, $1, $3); } | '(' expr ')' { $$ = $2; @@ -1373,7 +1484,7 @@ expr: | T_EXIT ctor_arguments { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_EXIT)); name->attr = ZEND_NAME_FQ; - $$ = zend_ast_create(ZEND_AST_CALL, name, $2); + $$ = zend_ast_create(ZEND_AST_CALL, name, $2, NULL); } | '@' expr { $$ = zend_ast_create(ZEND_AST_SILENCE, $2); } | scalar { $$ = $1; } @@ -1394,16 +1505,16 @@ expr: inline_function: - function returns_ref backup_doc_comment '(' parameter_list ')' lexical_vars return_type + function returns_ref backup_doc_comment optional_generic_type_parameter_list '(' parameter_list ')' lexical_vars return_type backup_fn_flags '{' inner_statement_list '}' backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_CLOSURE, $2 | $13, $1, $3, + { $$ = zend_ast_create_decl(ZEND_AST_CLOSURE, $2 | $14, $1, $3, NULL, - $5, $7, $11, $8, NULL); CG(extra_fn_flags) = $9; } - | fn returns_ref backup_doc_comment '(' parameter_list ')' return_type + $6, $8, $12, $9, NULL, $4); CG(extra_fn_flags) = $10; } + | fn returns_ref backup_doc_comment optional_generic_type_parameter_list '(' parameter_list ')' return_type T_DOUBLE_ARROW backup_fn_flags backup_lex_pos expr backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_ARROW_FUNC, $2 | $12, $1, $3, - NULL, $5, NULL, $11, $7, NULL); - CG(extra_fn_flags) = $9; } + { $$ = zend_ast_create_decl(ZEND_AST_ARROW_FUNC, $2 | $13, $1, $3, + NULL, $6, NULL, $12, $8, NULL, $4); + CG(extra_fn_flags) = $10; } ; fn: @@ -1447,20 +1558,20 @@ lexical_var: ; function_call: - name argument_list - { $$ = zend_ast_create(ZEND_AST_CALL, $1, $2); } + name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_CALL, $1, $3, $2); } | T_READONLY argument_list { zval zv; if (zend_lex_tstring(&zv, $1) == FAILURE) { YYABORT; } - $$ = zend_ast_create(ZEND_AST_CALL, zend_ast_create_zval(&zv), $2); + $$ = zend_ast_create(ZEND_AST_CALL, zend_ast_create_zval(&zv), $2, NULL); } - | class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list - { $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $4); } - | variable_class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list - { $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $4); } - | callable_expr { $$ = CG(zend_lineno); } argument_list { - $$ = zend_ast_create(ZEND_AST_CALL, $1, $3); - $$->lineno = $2; + | class_name T_PAAMAYIM_NEKUDOTAYIM member_name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $5, $4); } + | variable_class_name T_PAAMAYIM_NEKUDOTAYIM member_name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $5, $4); } + | callable_expr optional_call_type_argument_list { $$ = CG(zend_lineno); } argument_list { + $$ = zend_ast_create(ZEND_AST_CALL, $1, $4, $2); + $$->lineno = $3; } ; @@ -1571,10 +1682,10 @@ callable_variable: { $$ = zend_ast_create(ZEND_AST_VAR, $1); } | array_object_dereferenceable '[' optional_expr ']' { $$ = zend_ast_create(ZEND_AST_DIM, $1, $3); } - | array_object_dereferenceable T_OBJECT_OPERATOR property_name argument_list - { $$ = zend_ast_create(ZEND_AST_METHOD_CALL, $1, $3, $4); } - | array_object_dereferenceable T_NULLSAFE_OBJECT_OPERATOR property_name argument_list - { $$ = zend_ast_create(ZEND_AST_NULLSAFE_METHOD_CALL, $1, $3, $4); } + | array_object_dereferenceable T_OBJECT_OPERATOR property_name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_METHOD_CALL, $1, $3, $5, $4); } + | array_object_dereferenceable T_NULLSAFE_OBJECT_OPERATOR property_name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_NULLSAFE_METHOD_CALL, $1, $3, $5, $4); } | function_call { $$ = $1; } ; diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 07f2d44cb5c6..cf6ccfe8bbe7 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1610,6 +1610,10 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_ RETURN_TOKEN_WITH_STR(T_STRING, 0); } +"::<" { + RETURN_TOKEN(T_TURBOFISH); +} + "::" { RETURN_TOKEN(T_PAAMAYIM_NEKUDOTAYIM); } diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 538eff3ea34d..f5c8f11b3078 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -91,6 +91,9 @@ void init_op_array(zend_op_array *op_array, zend_function_type type, int initial op_array->num_dynamic_func_defs = 0; op_array->dynamic_func_defs = NULL; + op_array->generic_parameters = NULL; + op_array->generic_types = NULL; + ZEND_MAP_PTR_INIT(op_array->run_time_cache, NULL); op_array->cache_size = zend_op_array_extension_handles * sizeof(void*); @@ -118,11 +121,175 @@ ZEND_API void zend_type_release(zend_type type, bool persistent) { if (!ZEND_TYPE_USES_ARENA(type)) { pefree(ZEND_TYPE_LIST(type), persistent); } + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(type); + if (named->name) { + zend_string_release(named->name); + } + for (uint32_t i = 0; i < named->count; i++) { + zend_type_release(named->args[i], persistent); + } + pefree(named, persistent); + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + if (ref->name) { + zend_string_release(ref->name); + } + pefree(ref, persistent); } else if (ZEND_TYPE_HAS_NAME(type)) { zend_string_release(ZEND_TYPE_NAME(type)); } } +ZEND_API zend_generic_parameter_list *zend_generic_parameter_list_alloc(uint32_t count, bool persistent) { + ZEND_ASSERT(count > 0); + zend_generic_parameter_list *list = pemalloc(ZEND_GENERIC_PARAMETER_LIST_SIZE(count), persistent); + list->count = count; + for (uint32_t i = 0; i < count; i++) { + list->parameters[i].name = NULL; + list->parameters[i].variance = 0; + list->parameters[i].bound = (zend_type) ZEND_TYPE_INIT_NONE(0); + list->parameters[i].bound_pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + list->parameters[i].default_type = (zend_type) ZEND_TYPE_INIT_NONE(0); + list->parameters[i].default_pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + } + return list; +} + +ZEND_API void zend_generic_parameter_list_destroy(zend_generic_parameter_list *list) { + if (!list) { + return; + } + for (uint32_t i = 0; i < list->count; i++) { + zend_generic_parameter *param = &list->parameters[i]; + if (param->name) { + zend_string_release(param->name); + } + zend_type_release(param->bound, /* persistent */ false); + zend_type_release(param->bound_pre_erasure, /* persistent */ false); + zend_type_release(param->default_type, /* persistent */ false); + zend_type_release(param->default_pre_erasure, /* persistent */ false); + } + efree(list); +} + +static void zend_generic_type_table_value_dtor(zval *zv) { + zend_type *type = Z_PTR_P(zv); + zend_type_release(*type, /* persistent */ false); + efree(type); +} + +ZEND_API zend_generic_type_table *zend_generic_type_table_alloc(void) { + zend_generic_type_table *table = ecalloc(1, sizeof(zend_generic_type_table)); + return table; +} + +ZEND_API void zend_generic_type_table_destroy(zend_generic_type_table *table) { + if (!table || table->persisted) { + return; + } + + if (table->return_type) { + zend_type_release(*table->return_type, /* persistent */ false); + efree(table->return_type); + } + if (table->extends) { + zend_type_release(*table->extends, /* persistent */ false); + efree(table->extends); + } + if (table->parameters) { + zend_hash_destroy(table->parameters); + FREE_HASHTABLE(table->parameters); + } + if (table->properties) { + zend_hash_destroy(table->properties); + FREE_HASHTABLE(table->properties); + } + if (table->class_constants) { + zend_hash_destroy(table->class_constants); + FREE_HASHTABLE(table->class_constants); + } + if (table->implements) { + zend_hash_destroy(table->implements); + FREE_HASHTABLE(table->implements); + } + if (table->trait_uses) { + zend_hash_destroy(table->trait_uses); + FREE_HASHTABLE(table->trait_uses); + } + if (table->turbofish_args) { + zend_hash_destroy(table->turbofish_args); + FREE_HASHTABLE(table->turbofish_args); + } + efree(table); +} + +static HashTable *zend_generic_type_table_ensure_indexed(HashTable **slot) { + if (!*slot) { + HashTable *ht; + ALLOC_HASHTABLE(ht); + zend_hash_init(ht, 4, NULL, zend_generic_type_table_value_dtor, /* persistent */ false); + *slot = ht; + } + return *slot; +} + +static HashTable *zend_generic_type_table_ensure_named(HashTable **slot) { + if (!*slot) { + HashTable *ht; + ALLOC_HASHTABLE(ht); + zend_hash_init(ht, 4, NULL, zend_generic_type_table_value_dtor, /* persistent */ false); + *slot = ht; + } + return *slot; +} + +static zend_type *zend_type_box(zend_type type) { + zend_type *boxed = emalloc(sizeof(zend_type)); + *boxed = type; + return boxed; +} + +ZEND_API void zend_generic_type_table_set_return(zend_generic_type_table *t, zend_type type) { + if (t->return_type) { + zend_type_release(*t->return_type, /* persistent */ false); + efree(t->return_type); + } + t->return_type = zend_type_box(type); +} + +ZEND_API void zend_generic_type_table_set_extends(zend_generic_type_table *t, zend_type type) { + if (t->extends) { + zend_type_release(*t->extends, /* persistent */ false); + efree(t->extends); + } + t->extends = zend_type_box(type); +} + +ZEND_API void zend_generic_type_table_set_parameter(zend_generic_type_table *t, uint32_t idx, zend_type type) { + zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->parameters), idx, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_property(zend_generic_type_table *t, zend_string *name, zend_type type) { + zend_hash_update_ptr(zend_generic_type_table_ensure_named(&t->properties), name, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_class_constant(zend_generic_type_table *t, zend_string *name, zend_type type) { + zend_hash_update_ptr(zend_generic_type_table_ensure_named(&t->class_constants), name, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_implements(zend_generic_type_table *t, uint32_t idx, zend_type type) { + zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->implements), idx, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, uint32_t idx, zend_type type) { + zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->trait_uses), idx, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type) { + zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->turbofish_args), op_num, zend_type_box(type)); +} + ZEND_API void zend_free_internal_arg_info(zend_internal_function *function, bool persistent) { if (function->arg_info) { @@ -415,6 +582,14 @@ ZEND_API void destroy_zend_class(zval *zv) } } } + } else if (prop_info->flags & ZEND_ACC_GENERIC_CLONE) { + if (prop_info->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop_info->hooks[i]) { + destroy_op_array(&prop_info->hooks[i]->op_array); + } + } + } } } ZEND_HASH_FOREACH_END(); zend_hash_destroy(&ce->properties_info); @@ -441,6 +616,12 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->backed_enum_table) { zend_hash_release(ce->backed_enum_table); } + if (ce->generic_parameters) { + zend_generic_parameter_list_destroy(ce->generic_parameters); + } + if (ce->generic_types) { + zend_generic_type_table_destroy(ce->generic_types); + } break; case ZEND_INTERNAL_CLASS: if (ce->doc_comment) { @@ -663,6 +844,12 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) } efree(op_array->dynamic_func_defs); } + if (op_array->generic_parameters) { + zend_generic_parameter_list_destroy(op_array->generic_parameters); + } + if (op_array->generic_types) { + zend_generic_type_table_destroy(op_array->generic_types); + } } static void zend_update_extended_stmts(zend_op_array *op_array) diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 0edc4df37484..188b311dd1c6 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -127,14 +127,42 @@ typedef struct { zend_type types[1]; } zend_type_list; -#define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 25 -#define _ZEND_TYPE_MASK ((1u << 25) - 1) +/* Generic type-parameter reference. */ +typedef struct _zend_type_parameter_ref { + zend_string *name; /* type-parameter source name (e.g. "T") */ + uint32_t index; /* position in declaring entity's parameter list */ + uint8_t origin; /* one of zend_generic_origin (see zend_compile.h) */ +} zend_type_parameter_ref; + +/* List of pre-erasure type arguments attached to a named pre-erasure zend_type. */ +typedef struct _zend_type_arguments { + uint32_t count; + zend_type arguments[1]; +} zend_type_arguments; + +/* Pre-erasure named type with type arguments. */ +typedef struct _zend_type_named_with_args { + zend_string *name; /* class name */ + uint32_t name_attr; /* ZEND_NAME_NOT_FQ / ZEND_NAME_FQ / ZEND_NAME_RELATIVE */ + uint32_t count; /* number of type arguments */ + zend_type args[1]; /* flexible array of pre-erasure type arguments */ +} zend_type_named_with_args; + +#define ZEND_TYPE_NAMED_WITH_ARGS_SIZE(count) \ + (sizeof(zend_type_named_with_args) + ((count) - 1) * sizeof(zend_type)) + +#define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 26 +#define _ZEND_TYPE_MASK (((1u << 26) - 1) | _ZEND_TYPE_NAMED_WITH_ARGS_BIT) +/* Pre-erasure named type with type arguments. Side-table only. */ +#define _ZEND_TYPE_NAMED_WITH_ARGS_BIT (1u << 31) +/* Generic type-parameter reference. */ +#define _ZEND_TYPE_TYPE_PARAMETER_BIT (1u << 25) /* 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_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT|_ZEND_TYPE_TYPE_PARAMETER_BIT|_ZEND_TYPE_NAMED_WITH_ARGS_BIT) /* For BC behaviour with iterable type */ #define _ZEND_TYPE_ITERABLE_BIT (1u << 21) /* Whether the type list is arena allocated */ @@ -165,6 +193,18 @@ typedef struct { #define ZEND_TYPE_HAS_LIST(t) \ ((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0) +#define ZEND_TYPE_HAS_TYPE_PARAMETER(t) \ + ((((t).type_mask) & _ZEND_TYPE_TYPE_PARAMETER_BIT) != 0) + +#define ZEND_TYPE_TYPE_PARAMETER(t) \ + ((zend_type_parameter_ref *) (t).ptr) + +#define ZEND_TYPE_HAS_NAMED_WITH_ARGS(t) \ + ((((t).type_mask) & _ZEND_TYPE_NAMED_WITH_ARGS_BIT) != 0) + +#define ZEND_TYPE_NAMED_WITH_ARGS(t) \ + ((zend_type_named_with_args *) (t).ptr) + #define ZEND_TYPE_IS_ITERABLE_FALLBACK(t) \ ((((t).type_mask) & _ZEND_TYPE_ITERABLE_BIT) != 0) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index a0bfad488f85..0271675af476 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -8931,6 +8931,41 @@ ZEND_VM_HOT_HANDLER(211, ZEND_TYPE_ASSERT, CONST, ANY, NUM) ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } +ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + + SAVE_OPLINE(); + + if (OP1_TYPE == IS_UNUSED) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + zend_check_generic_new_arguments(ce, arity, args_box); + } + + if (UNEXPECTED(EG(exception))) { + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + ZEND_VM_HOT_HANDLER(122, ZEND_DEFINED, CONST, ANY, CACHE_SLOT) { USE_OPLINE diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index cedc735bbb1e..556a3594d710 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -21994,6 +21994,41 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_YIELD_SPEC_TM ZEND_VM_RETURN(); } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + + SAVE_OPLINE(); + + if (IS_TMP_VAR == IS_UNUSED) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + zend_check_generic_new_arguments(ce, arity, args_box); + } + + if (UNEXPECTED(EG(exception))) { + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_COUNT_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -37311,6 +37346,41 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_YIELD_SPEC_UN ZEND_VM_RETURN(); } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + + SAVE_OPLINE(); + + if (IS_UNUSED == IS_UNUSED) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + zend_check_generic_new_arguments(ce, arity, args_box); + } + + if (UNEXPECTED(EG(exception))) { + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -74481,6 +74551,41 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_YIELD_SPEC_TMP_UNU ZEND_VM_RETURN(); } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + + SAVE_OPLINE(); + + if (IS_TMP_VAR == IS_UNUSED) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + zend_check_generic_new_arguments(ce, arity, args_box); + } + + if (UNEXPECTED(EG(exception))) { + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_COUNT_SPEC_TMP_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -89798,6 +89903,41 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_YIELD_SPEC_UNUSED_ ZEND_VM_RETURN(); } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + + SAVE_OPLINE(); + + if (IS_UNUSED == IS_UNUSED) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + zend_check_generic_new_arguments(ce, arity, args_box); + } + + if (UNEXPECTED(EG(exception))) { + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -109273,6 +109413,11 @@ ZEND_API void execute_ex(zend_execute_data *ex) (void*)&&ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED_LABEL, (void*)&&ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST_LABEL, (void*)&&ZEND_TYPE_ASSERT_SPEC_CONST_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, (void*)&&ZEND_INIT_FCALL_OFFSET_SPEC_CONST_LABEL, (void*)&&ZEND_RECV_NOTYPE_SPEC_LABEL, (void*)&&ZEND_NULL_LABEL, @@ -113005,6 +113150,11 @@ ZEND_API void execute_ex(zend_execute_data *ex) ZEND_YIELD_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); VM_TRACE_OP_END(ZEND_YIELD_SPEC_TMP_UNUSED) HYBRID_BREAK(); + HYBRID_CASE(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED): + VM_TRACE(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) + HYBRID_BREAK(); HYBRID_CASE(ZEND_COUNT_SPEC_TMP_UNUSED): VM_TRACE(ZEND_COUNT_SPEC_TMP_UNUSED) ZEND_COUNT_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); @@ -114110,6 +114260,11 @@ ZEND_API void execute_ex(zend_execute_data *ex) ZEND_YIELD_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); VM_TRACE_OP_END(ZEND_YIELD_SPEC_UNUSED_UNUSED) HYBRID_BREAK(); + HYBRID_CASE(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED): + VM_TRACE(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) + HYBRID_BREAK(); HYBRID_CASE(ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED): VM_TRACE(ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED) ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); @@ -118211,6 +118366,11 @@ void zend_vm_init(void) ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED_HANDLER, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST_HANDLER, ZEND_TYPE_ASSERT_SPEC_CONST_HANDLER, + ZEND_NULL_HANDLER, + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_HANDLER, + ZEND_NULL_HANDLER, + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_HANDLER, + ZEND_NULL_HANDLER, ZEND_INIT_FCALL_OFFSET_SPEC_CONST_HANDLER, ZEND_RECV_NOTYPE_SPEC_HANDLER, ZEND_NULL_HANDLER, @@ -121689,6 +121849,11 @@ void zend_vm_init(void) ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED_TAILCALL_HANDLER, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST_TAILCALL_HANDLER, ZEND_TYPE_ASSERT_SPEC_CONST_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, ZEND_INIT_FCALL_OFFSET_SPEC_CONST_TAILCALL_HANDLER, ZEND_RECV_NOTYPE_SPEC_TAILCALL_HANDLER, ZEND_NULL_TAILCALL_HANDLER, @@ -122657,7 +122822,7 @@ void zend_vm_init(void) 1255, 1256 | SPEC_RULE_OP1, 1261 | SPEC_RULE_OP1, - 3474, + 3479, 1266 | SPEC_RULE_OP1, 1271 | SPEC_RULE_OP1, 1276 | SPEC_RULE_OP2, @@ -122691,7 +122856,7 @@ void zend_vm_init(void) 1559 | SPEC_RULE_OP1 | SPEC_RULE_OP2, 1584 | SPEC_RULE_OP1, 1589, - 3474, + 3479, 1590 | SPEC_RULE_OP1, 1595 | SPEC_RULE_OP1 | SPEC_RULE_OP2, 1620 | SPEC_RULE_OP1 | SPEC_RULE_OP2, @@ -122824,50 +122989,50 @@ void zend_vm_init(void) 2556, 2557, 2558, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, + 2559 | SPEC_RULE_OP1, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, + 3479, }; #if 0 #elif (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) @@ -123060,7 +123225,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2567 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2572 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123068,7 +123233,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2592 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2597 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123076,7 +123241,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2617 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2622 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123087,17 +123252,17 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2642 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2647 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } else if (op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2667 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2672 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2692 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2697 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } break; case ZEND_MUL: @@ -123108,17 +123273,17 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2717 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2722 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2742 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2747 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2767 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2772 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_IDENTICAL: @@ -123129,16 +123294,16 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2792 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2797 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2867 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2872 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0)) { - spec = 3092 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3097 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { - spec = 3098 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 3103 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_NOT_IDENTICAL: @@ -123149,16 +123314,16 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2942 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2947 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3017 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3022 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0)) { - spec = 3095 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3100 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { - spec = 3103 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 3108 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_EQUAL: @@ -123169,12 +123334,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2792 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2797 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2867 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2872 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_NOT_EQUAL: @@ -123185,12 +123350,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2942 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2947 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3017 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3022 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_SMALLER: @@ -123198,12 +123363,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3108 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3113 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3183 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3188 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } break; case ZEND_IS_SMALLER_OR_EQUAL: @@ -123211,79 +123376,79 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3258 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3263 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3333 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3338 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } break; case ZEND_QM_ASSIGN: if (op1_info == MAY_BE_LONG) { - spec = 3420 | SPEC_RULE_OP1; - } else if (op1_info == MAY_BE_DOUBLE) { spec = 3425 | SPEC_RULE_OP1; - } else if ((op->op1_type == IS_CONST) ? !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1)) : (!(op1_info & ((MAY_BE_ANY|MAY_BE_UNDEF)-(MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE))))) { + } else if (op1_info == MAY_BE_DOUBLE) { spec = 3430 | SPEC_RULE_OP1; + } else if ((op->op1_type == IS_CONST) ? !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1)) : (!(op1_info & ((MAY_BE_ANY|MAY_BE_UNDEF)-(MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE))))) { + spec = 3435 | SPEC_RULE_OP1; } break; case ZEND_PRE_INC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3408 | SPEC_RULE_RETVAL; + spec = 3413 | SPEC_RULE_RETVAL; } else if (op1_info == MAY_BE_LONG) { - spec = 3410 | SPEC_RULE_RETVAL; + spec = 3415 | SPEC_RULE_RETVAL; } break; case ZEND_PRE_DEC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3412 | SPEC_RULE_RETVAL; + spec = 3417 | SPEC_RULE_RETVAL; } else if (op1_info == MAY_BE_LONG) { - spec = 3414 | SPEC_RULE_RETVAL; + spec = 3419 | SPEC_RULE_RETVAL; } break; case ZEND_POST_INC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3416; + spec = 3421; } else if (op1_info == MAY_BE_LONG) { - spec = 3417; + spec = 3422; } break; case ZEND_POST_DEC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3418; + spec = 3423; } else if (op1_info == MAY_BE_LONG) { - spec = 3419; + spec = 3424; } break; case ZEND_JMP: if (OP_JMP_ADDR(op, op->op1) > op) { - spec = 2566; + spec = 2571; } break; case ZEND_INIT_FCALL: if (Z_EXTRA_P(RT_CONSTANT(op, op->op2)) != 0) { - spec = 2559; + spec = 2564; } break; case ZEND_RECV: if (op->op2.num == MAY_BE_ANY) { - spec = 2560; + spec = 2565; } break; case ZEND_SEND_VAL: if (op->op1_type == IS_CONST && op->op2_type == IS_UNUSED && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1))) { - spec = 3470; + spec = 3475; } break; case ZEND_SEND_VAR_EX: if (op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0) { - spec = 3465 | SPEC_RULE_OP1; + spec = 3470 | SPEC_RULE_OP1; } break; case ZEND_FE_FETCH_R: if (op->op2_type == IS_CV && (op1_info & (MAY_BE_ANY|MAY_BE_REF)) == MAY_BE_ARRAY) { - spec = 3472 | SPEC_RULE_RETVAL; + spec = 3477 | SPEC_RULE_RETVAL; } break; case ZEND_FETCH_DIM_R: @@ -123291,22 +123456,22 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3435 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 3440 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } break; case ZEND_SEND_VAL_EX: if (op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && op->op1_type == IS_CONST && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1))) { - spec = 3471; + spec = 3476; } break; case ZEND_SEND_VAR: if (op->op2_type == IS_UNUSED && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0) { - spec = 3460 | SPEC_RULE_OP1; + spec = 3465 | SPEC_RULE_OP1; } break; case ZEND_COUNT: if ((op1_info & (MAY_BE_ANY|MAY_BE_UNDEF|MAY_BE_REF)) == MAY_BE_ARRAY) { - spec = 2561 | SPEC_RULE_OP1; + spec = 2566 | SPEC_RULE_OP1; } break; case ZEND_BW_OR: diff --git a/Zend/zend_vm_handlers.h b/Zend/zend_vm_handlers.h index 6f1595195450..195a8d5b4dac 100644 --- a/Zend/zend_vm_handlers.h +++ b/Zend/zend_vm_handlers.h @@ -1087,507 +1087,509 @@ _(2556, ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED) \ _(2557, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST) \ _(2558, ZEND_TYPE_ASSERT_SPEC_CONST) \ - _(2559, ZEND_INIT_FCALL_OFFSET_SPEC_CONST) \ - _(2560, ZEND_RECV_NOTYPE_SPEC) \ - _(2562, ZEND_COUNT_ARRAY_SPEC_TMP_UNUSED) \ - _(2565, ZEND_COUNT_ARRAY_SPEC_CV_UNUSED) \ - _(2566, ZEND_JMP_FORWARD_SPEC) \ - _(2572, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2573, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2574, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2576, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2560, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) \ + _(2562, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) \ + _(2564, ZEND_INIT_FCALL_OFFSET_SPEC_CONST) \ + _(2565, ZEND_RECV_NOTYPE_SPEC) \ + _(2567, ZEND_COUNT_ARRAY_SPEC_TMP_UNUSED) \ + _(2570, ZEND_COUNT_ARRAY_SPEC_CV_UNUSED) \ + _(2571, ZEND_JMP_FORWARD_SPEC) \ _(2577, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ _(2578, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2579, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2581, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2587, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2588, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2589, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2591, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2597, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ - _(2598, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2599, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2601, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2582, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2583, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2584, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2586, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2592, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2593, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2594, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2596, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2602, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ _(2603, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2604, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2606, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2612, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ - _(2613, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2614, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2616, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2622, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2623, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2624, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2626, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2607, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2608, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2609, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2611, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2617, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2618, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2619, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2621, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2627, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ _(2628, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2629, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2631, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2637, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2638, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2639, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2641, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2643, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2644, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2646, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2647, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2648, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2649, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2651, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2632, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2633, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2634, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2636, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2642, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2643, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2644, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2646, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2648, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2649, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2651, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ _(2652, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ _(2653, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2654, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2656, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2662, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2663, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2664, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2666, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2668, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2669, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2671, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2672, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ - _(2673, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2674, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2676, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2657, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2658, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2659, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2661, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2667, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2668, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2669, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2671, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2673, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2674, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2676, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ _(2677, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ _(2678, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2679, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2681, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2687, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ - _(2688, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2689, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2691, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2693, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2694, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2696, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2697, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2698, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2699, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2701, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2682, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2683, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2684, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2686, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2692, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2693, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2694, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2696, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2698, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2699, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2701, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ _(2702, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ _(2703, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2704, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2706, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2712, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2713, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2714, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2716, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2722, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2723, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2724, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2726, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2707, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2708, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2709, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2711, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2717, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2718, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2719, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2721, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2727, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ _(2728, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2729, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2731, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2737, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2738, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2739, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2741, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2747, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ - _(2748, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2749, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2751, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2732, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2733, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2734, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2736, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2742, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2743, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2744, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2746, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2752, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ _(2753, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2754, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2756, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2762, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ - _(2763, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2764, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2766, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2772, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2773, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2774, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2776, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2757, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2758, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2759, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2761, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2767, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2768, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2769, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2771, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2777, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ _(2778, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2779, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2781, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2787, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2788, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2789, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2791, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2807, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2808, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2809, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2810, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2811, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2812, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2813, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2814, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2815, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2819, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2820, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2821, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2822, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2823, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2824, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2825, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2826, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2827, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2828, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2829, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2830, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2834, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2835, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2836, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2852, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2853, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2854, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2855, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2856, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2857, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2858, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2859, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2860, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2864, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2865, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2866, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2882, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2883, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2884, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2885, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2886, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2887, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2888, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2889, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2890, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2894, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2895, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2896, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2897, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2898, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2899, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2900, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2901, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2902, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2903, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2904, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2905, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2909, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2910, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2911, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2927, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2928, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2929, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2930, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2931, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2932, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2933, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2934, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2935, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2939, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2940, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2941, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2957, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2958, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2959, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2960, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2961, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2962, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2963, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2964, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2965, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2969, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2970, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2971, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2972, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2973, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2974, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2975, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2976, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2977, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2978, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2979, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2980, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2984, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2985, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2986, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3002, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3003, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3004, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3005, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3006, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3007, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3008, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3009, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3010, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3014, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3015, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3016, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3032, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3033, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3034, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3035, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3036, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3037, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3038, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3039, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3040, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3044, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3045, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3046, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3047, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3048, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3049, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3050, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3051, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3052, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3053, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3054, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3055, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3059, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3060, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3061, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3077, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3078, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3079, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3080, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3081, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3082, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3083, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3084, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3085, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3089, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3090, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3091, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3092, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ - _(3093, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3094, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3095, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ - _(3096, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3097, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3098, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ - _(3102, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CV) \ - _(3103, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ - _(3107, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CV) \ - _(3111, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3112, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3113, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3114, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3115, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3116, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3120, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3121, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3122, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3123, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3124, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3125, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3126, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3127, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3128, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3129, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3130, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3131, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3135, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3136, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3137, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3138, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3139, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3140, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3141, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3142, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3143, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3144, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3145, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3146, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3150, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3151, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3152, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3168, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3169, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3170, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3171, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3172, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3173, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3174, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3175, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3176, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3180, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3181, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3182, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3186, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3187, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3188, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3189, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3190, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3191, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3195, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3196, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3197, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3198, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3199, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3200, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3201, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3202, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3203, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3204, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3205, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3206, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3210, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3211, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3212, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3213, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3214, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3215, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3216, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3217, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3218, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3219, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3220, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3221, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3225, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3226, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3227, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3243, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3244, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3245, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3246, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3247, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3248, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3249, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3250, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3251, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3255, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3256, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3257, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3261, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3262, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3263, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3264, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3265, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3266, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3270, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3271, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3272, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3273, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3274, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3275, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3276, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3277, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3278, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3279, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3280, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3281, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3285, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3286, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3287, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3288, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3289, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3290, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3291, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3292, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3293, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3294, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3295, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3296, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3300, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3301, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3302, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3318, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3319, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3320, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3321, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3322, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3323, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3324, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3325, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3326, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3330, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3331, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3332, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3336, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3337, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3338, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3339, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3340, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3341, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3345, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3346, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3347, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3348, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3349, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3350, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3351, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3352, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3353, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3354, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3355, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3356, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3360, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3361, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3362, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3363, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3364, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3365, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3366, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3367, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3368, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3369, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3370, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3371, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3375, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3376, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3377, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3393, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3394, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3395, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3396, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3397, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3398, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3399, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3400, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3401, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3405, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3406, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3407, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3408, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ - _(3409, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ - _(3410, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_UNUSED) \ - _(3411, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_USED) \ - _(3412, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ - _(3413, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ - _(3414, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_UNUSED) \ - _(3415, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_USED) \ - _(3416, ZEND_POST_INC_LONG_NO_OVERFLOW_SPEC_CV) \ - _(3417, ZEND_POST_INC_LONG_SPEC_CV) \ - _(3418, ZEND_POST_DEC_LONG_NO_OVERFLOW_SPEC_CV) \ - _(3419, ZEND_POST_DEC_LONG_SPEC_CV) \ - _(3420, ZEND_QM_ASSIGN_LONG_SPEC_CONST) \ - _(3421, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3422, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3424, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3425, ZEND_QM_ASSIGN_DOUBLE_SPEC_CONST) \ - _(3426, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3427, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3429, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3430, ZEND_QM_ASSIGN_NOREF_SPEC_CONST) \ - _(3431, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3432, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3434, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3436, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3437, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3439, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3440, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ - _(3441, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3442, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3444, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(2782, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2783, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2784, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2786, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2792, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2793, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2794, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2796, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2812, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2813, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2814, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2815, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2816, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2817, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2818, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2819, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2820, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2824, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2825, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2826, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2827, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2828, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2829, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2830, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2831, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2832, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2833, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2834, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2835, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2839, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2840, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2841, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2857, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2858, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2859, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2860, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2861, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2862, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2863, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2864, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2865, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2869, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2870, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2871, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2887, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2888, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2889, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2890, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2891, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2892, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2893, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2894, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2895, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2899, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2900, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2901, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2902, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2903, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2904, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2905, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2906, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2907, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2908, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2909, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2910, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2914, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2915, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2916, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2932, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2933, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2934, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2935, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2936, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2937, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2938, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2939, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2940, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2944, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2945, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2946, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2962, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2963, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2964, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2965, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2966, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2967, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2968, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2969, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2970, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2974, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2975, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2976, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2977, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2978, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2979, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2980, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2981, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2982, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2983, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2984, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2985, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2989, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2990, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2991, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3007, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3008, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3009, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3010, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3011, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3012, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3013, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3014, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3015, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3019, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3020, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3021, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3037, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3038, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3039, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3040, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3041, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3042, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3043, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3044, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3045, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3049, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3050, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3051, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3052, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3053, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3054, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3055, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3056, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3057, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3058, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3059, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3060, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3064, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3065, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3066, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3082, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3083, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3084, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3085, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3086, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3087, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3088, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3089, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3090, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3094, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3095, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3096, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3097, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ + _(3098, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3099, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3100, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ + _(3101, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3102, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3103, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ + _(3107, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CV) \ + _(3108, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ + _(3112, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CV) \ + _(3116, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3117, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3118, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3119, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3120, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3121, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3125, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3126, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3127, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3128, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3129, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3130, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3131, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3132, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3133, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3134, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3135, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3136, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3140, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3141, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3142, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3143, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3144, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3145, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3146, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3147, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3148, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3149, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3150, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3151, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3155, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3156, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3157, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3173, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3174, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3175, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3176, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3177, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3178, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3179, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3180, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3181, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3185, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3186, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3187, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3191, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3192, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3193, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3194, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3195, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3196, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3200, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3201, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3202, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3203, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3204, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3205, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3206, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3207, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3208, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3209, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3210, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3211, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3215, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3216, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3217, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3218, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3219, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3220, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3221, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3222, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3223, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3224, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3225, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3226, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3230, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3231, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3232, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3248, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3249, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3250, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3251, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3252, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3253, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3254, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3255, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3256, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3260, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3261, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3262, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3266, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3267, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3268, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3269, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3270, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3271, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3275, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3276, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3277, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3278, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3279, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3280, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3281, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3282, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3283, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3284, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3285, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3286, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3290, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3291, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3292, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3293, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3294, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3295, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3296, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3297, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3298, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3299, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3300, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3301, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3305, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3306, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3307, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3323, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3324, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3325, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3326, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3327, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3328, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3329, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3330, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3331, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3335, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3336, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3337, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3341, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3342, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3343, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3344, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3345, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3346, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3350, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3351, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3352, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3353, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3354, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3355, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3356, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3357, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3358, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3359, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3360, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3361, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3365, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3366, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3367, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3368, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3369, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3370, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3371, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3372, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3373, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3374, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3375, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3376, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3380, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3381, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3382, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3398, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3399, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3400, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3401, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3402, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3403, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3404, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3405, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3406, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3410, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3411, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3412, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3413, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ + _(3414, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ + _(3415, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_UNUSED) \ + _(3416, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_USED) \ + _(3417, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ + _(3418, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ + _(3419, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_UNUSED) \ + _(3420, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_USED) \ + _(3421, ZEND_POST_INC_LONG_NO_OVERFLOW_SPEC_CV) \ + _(3422, ZEND_POST_INC_LONG_SPEC_CV) \ + _(3423, ZEND_POST_DEC_LONG_NO_OVERFLOW_SPEC_CV) \ + _(3424, ZEND_POST_DEC_LONG_SPEC_CV) \ + _(3425, ZEND_QM_ASSIGN_LONG_SPEC_CONST) \ + _(3426, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3427, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3429, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3430, ZEND_QM_ASSIGN_DOUBLE_SPEC_CONST) \ + _(3431, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3432, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3434, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3435, ZEND_QM_ASSIGN_NOREF_SPEC_CONST) \ + _(3436, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3437, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3439, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3441, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3442, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3444, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ _(3445, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ _(3446, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ _(3447, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ _(3449, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3455, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_CONST) \ - _(3456, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3457, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3459, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3462, ZEND_SEND_VAR_SIMPLE_SPEC_VAR) \ - _(3464, ZEND_SEND_VAR_SIMPLE_SPEC_CV) \ - _(3467, ZEND_SEND_VAR_EX_SIMPLE_SPEC_VAR_UNUSED) \ - _(3469, ZEND_SEND_VAR_EX_SIMPLE_SPEC_CV_UNUSED) \ - _(3470, ZEND_SEND_VAL_SIMPLE_SPEC_CONST) \ - _(3471, ZEND_SEND_VAL_EX_SIMPLE_SPEC_CONST) \ - _(3472, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_UNUSED) \ - _(3473, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) \ - _(3473+1, ZEND_NULL) + _(3450, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ + _(3451, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3452, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3454, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3460, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_CONST) \ + _(3461, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3462, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3464, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3467, ZEND_SEND_VAR_SIMPLE_SPEC_VAR) \ + _(3469, ZEND_SEND_VAR_SIMPLE_SPEC_CV) \ + _(3472, ZEND_SEND_VAR_EX_SIMPLE_SPEC_VAR_UNUSED) \ + _(3474, ZEND_SEND_VAR_EX_SIMPLE_SPEC_CV_UNUSED) \ + _(3475, ZEND_SEND_VAL_SIMPLE_SPEC_CONST) \ + _(3476, ZEND_SEND_VAL_EX_SIMPLE_SPEC_CONST) \ + _(3477, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_UNUSED) \ + _(3478, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) \ + _(3478+1, ZEND_NULL) diff --git a/Zend/zend_vm_opcodes.c b/Zend/zend_vm_opcodes.c index 0ece3e6f0c66..accebcfae814 100644 --- a/Zend/zend_vm_opcodes.c +++ b/Zend/zend_vm_opcodes.c @@ -21,7 +21,7 @@ #include #include -static const char *zend_vm_opcodes_names[212] = { +static const char *zend_vm_opcodes_names[213] = { "ZEND_NOP", "ZEND_ADD", "ZEND_SUB", @@ -234,9 +234,10 @@ static const char *zend_vm_opcodes_names[212] = { "ZEND_INIT_PARENT_PROPERTY_HOOK_CALL", "ZEND_DECLARE_ATTRIBUTED_CONST", "ZEND_TYPE_ASSERT", + "ZEND_VERIFY_GENERIC_ARGUMENTS", }; -static uint32_t zend_vm_opcodes_flags[212] = { +static uint32_t zend_vm_opcodes_flags[213] = { 0x00000000, 0x00000b0b, 0x00000b0b, @@ -449,6 +450,7 @@ static uint32_t zend_vm_opcodes_flags[212] = { 0x01001103, 0x00000303, 0x01000003, + 0x00000101, }; ZEND_API const char* ZEND_FASTCALL zend_get_opcode_name(uint8_t opcode) { diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h index 92b46e6628f3..b60aad72e1ba 100644 --- a/Zend/zend_vm_opcodes.h +++ b/Zend/zend_vm_opcodes.h @@ -331,7 +331,8 @@ END_EXTERN_C() #define ZEND_INIT_PARENT_PROPERTY_HOOK_CALL 209 #define ZEND_DECLARE_ATTRIBUTED_CONST 210 #define ZEND_TYPE_ASSERT 211 +#define ZEND_VERIFY_GENERIC_ARGUMENTS 212 -#define ZEND_VM_LAST_OPCODE 211 +#define ZEND_VM_LAST_OPCODE 212 #endif diff --git a/docs/source/core/generics.rst b/docs/source/core/generics.rst new file mode 100644 index 000000000000..fdc0cf97210e --- /dev/null +++ b/docs/source/core/generics.rst @@ -0,0 +1,241 @@ +############### + Generic types +############### + +PHP supports generic type parameters on classes, interfaces, traits, functions, methods, closures, +and arrow functions. They are *bound-erased*: at runtime, each type parameter is replaced by its +declared bound (or ``mixed`` when unbounded), so the engine sees ordinary PHP types. The +pre-erasure form is kept in a per-entity side table and consulted by the linker, the runtime +turbofish check, and Reflection. Existing handlers - parameter checks, return-type verification, +property writes - operate on the erased type and never see a ``T``-ref. + +*************************** + Pre-erasure type carriers +*************************** + +``zend_type`` carries the pre-erasure shapes generics need. Two bits in ``type_mask`` distinguish +them: + +.. code:: c + + #define _ZEND_TYPE_TYPE_PARAMETER_BIT (1u << 25) /* t.ptr is a zend_type_parameter_ref * */ + #define _ZEND_TYPE_NAMED_WITH_ARGS_BIT (1u << 31) /* t.ptr is a zend_type_named_with_args * */ + +When ``_ZEND_TYPE_TYPE_PARAMETER_BIT`` is set, ``t.ptr`` is a reference to a declared parameter; +when ``_ZEND_TYPE_NAMED_WITH_ARGS_BIT`` is set, ``t.ptr`` is a generic application like +``Foo``: + +.. code:: c + + typedef struct _zend_type_parameter_ref { + zend_string *name; /* "T" */ + uint32_t index; /* position in the parameter list */ + uint8_t origin; /* CLASS_LIKE or FUNCTION_LIKE */ + } zend_type_parameter_ref; + + typedef struct _zend_type_named_with_args { + zend_string *name; /* "Foo", or NULL for synthetic turbofish carriers */ + uint32_t name_attr; + uint32_t count; + zend_type args[1]; /* flexible array */ + } zend_type_named_with_args; + +The ``origin`` field on a ``T``-ref tells you which scope to resolve ``index`` in. NAMED_WITH_ARGS +values with ``name == NULL`` are synthetic carriers that hold the args at a turbofish site - +they're not type expressions, just a payload. Both payloads only appear in side-table slots, are +heap-allocated, and are released through ``zend_type_release``. + +********************** + Per-entity metadata +********************** + +Both ``zend_class_entry`` and ``zend_op_array`` carry two pointer fields for generic information, +both ``NULL`` on entities that don't use generics: + +.. code:: c + + zend_generic_parameter_list *generic_parameters; /* the entity's own declaration */ + zend_generic_type_table *generic_types; /* pre-erasure metadata for everything else */ + +``generic_parameters`` is the entity's own declaration list (a flexible-array struct allocated +through ``zend_generic_parameter_list_alloc``). Each parameter: + +.. code:: c + + typedef struct _zend_generic_parameter { + zend_string *name; + zend_generic_variance variance; /* INVARIANT, COVARIANT, CONTRAVARIANT */ + zend_type bound; /* erased view; NONE if unbounded */ + zend_type bound_pre_erasure; /* NONE if same as bound */ + zend_type default_type; + zend_type default_pre_erasure; + } zend_generic_parameter; + +``generic_types`` collects every other place a pre-erasure type might need to be looked up: + +.. code:: c + + typedef struct _zend_generic_type_table { + zend_type *return_type; + zend_type *extends; + HashTable *parameters; /* parameter index -> zend_type * */ + HashTable *properties; /* zend_string * -> zend_type * */ + HashTable *class_constants; + HashTable *implements; /* implements index -> zend_type * */ + HashTable *trait_uses; /* trait-use index -> zend_type * */ + HashTable *turbofish_args; /* opline->extended_value -> zend_type * */ + } zend_generic_type_table; + +Every slot is independently NULLable. The table is allocated lazily by +``zend_generic_get_or_create_class_table`` / ``..._op_array_table`` on first use, and individual +slots stay ``NULL`` whenever the pre-erasure form would be byte-equal to the erased one. You +populate slots through the ``zend_generic_type_table_set_*`` family; each setter takes ownership +of the ``zend_type`` you hand it. + +The ``turbofish_args`` table is keyed by ``opline->extended_value`` rather than the opline's +position in the bytecode array. Optimizer passes reorder, insert, and delete opcodes; the +``extended_value`` operand belongs to the opline itself, so it survives reordering. + +Attributes carry their turbofish on two fields on ``zend_attribute``: + +.. code:: c + + uint8_t generic_arity; + zend_type *generic_args; /* NAMED_WITH_ARGS holding turbofish args */ + +****************** + Compile-time flow +****************** + +When the compiler reaches a generic declaration, ``zend_compile_generic_type_parameter_list`` +builds the parameter list and pushes a linked-list scope entry. ``T``-ref resolution walks the +chain through ``zend_generic_lookup``. The scope entry's ``self_compiling`` field points at the +parameter whose bound or default is currently being compiled, which is what lets the compiler +reject ``class A`` (top-level self-reference) while accepting +``class A>`` (the inner ``T`` is nested under another generic). + +Every type expression carrying a ``T`` is compiled twice: once as the runtime-erased ``zend_type`` +on the entity's ordinary slot (``arg_info::type``, ``zend_property_info::type``), and once in +pre-erasure form on the matching ``generic_types`` slot. When the bound is a list type (union, +intersection, or DNF), the erased copy is built through ``zend_arena_deep_copy_type_list`` so +nested type lists are not aliased between ``param->bound`` and the consuming ``arg_info``. + +For a turbofish at a call or ``new`` site, ``zend_emit_verify_generic_arguments`` compiles the +args into a NAMED_WITH_ARGS payload, stores it in ``op_array->generic_types->turbofish_args`` +under a fresh ``args_id``, and emits a ``ZEND_VERIFY_GENERIC_ARGUMENTS`` opcode with +``op2.num = arity`` and ``extended_value = args_id``. The opcode sits between the call's +``INIT_*`` (or ``ZEND_NEW``) and its ``DO_*``, so the call frame is set up when the handler runs. + +****************************** + Variance and static context +****************************** + +After a class or function with non-invariant generic parameters compiles, +``zend_check_generic_variance_markers`` and ``zend_check_function_variance_markers`` walk every +position where a ``T`` can appear (signatures, properties, hook signatures, the type-args at +``extends``/``implements``/``use``, bounds, defaults). Both go through ``zend_variance_walk``, +which dispatches each ``T``-ref to the matching parameter list by ``ref->origin``. + +Polarity composes through nested generics: outer × slot, with INVARIANT absorbing. Method return +types are covariant, parameters contravariant; readonly and get-only-hooked properties covariant, +set-only-hooked contravariant, r/w and get+set hooked invariant; bounds and defaults always +invariant. Constructors are exempt. Violations produce ``Type parameter T declared covariant +(+T) cannot appear in contravariant position``. + +The static-context check is structural: ``zend_check_class_origin_in_static_context`` rejects any +class-origin ``T``-ref that resolves while ``CG(in_static_member_type)`` is set. The flag is set +in ``zend_compile_func_decl`` around the signature of a static method and in +``zend_compile_prop_decl`` around the type of a static property - a static member has no instance, +so an instance-bound parameter has no binding to consult. + +***************** + Runtime handler +***************** + +The ``ZEND_VERIFY_GENERIC_ARGUMENTS`` handler lives in ``zend_vm_def.h``. It looks up the captured +args via ``zend_generic_get_turbofish_args`` and dispatches to ``zend_check_generic_call_arguments`` +(call) or ``zend_check_generic_new_arguments`` (instantiation). Both validate arity against the +callee's ``generic_parameters``, then walk the carrier and call +``zend_check_generic_arg_satisfies_bound`` for each ``(arg, parameter)`` pair. Mismatches throw +``ArgumentCountError`` or ``TypeError``; on exception, teardown releases the call frame, the +trampoline name if any, and ``$this`` if held. + +************************ + Inheritance and linking +************************ + +When a child extends a generic ancestor or uses a generic trait, the linker substitutes the +inherited prototype's pre-erasure types with the child's bindings before the variance check runs. +``zend_get_inheritance_binding`` returns the *direct* binding (the args at ce's own +``extends``/``implements``/``use`` clause); for ``extends`` it matches by pointer when +``ZEND_ACC_RESOLVED_PARENT`` is set and by name otherwise, since the override check fires before +parent resolution on the deferred-obligation path. ``zend_get_inheritance_binding_full`` walks +the parent chain and direct interfaces, composing each link through +``zend_substitute_leaf_type_param`` and looking the parent up by name if it isn't yet resolved. + +``zend_substitute_leaf_type_param`` replaces a class-scope ``T``-ref with ``args[T->index]`` and +propagates the nullable bit, so ``?T`` with ``T = int`` substitutes to ``?int``. Other modifier +bits are not propagated; non-leaf carriers (NAMED_WITH_ARGS, lists) are returned unchanged. +``zend_substitute_proto_type`` is the entry point used by ``zend_do_perform_implementation_check``; +it returns the unsubstituted fallback when ce isn't generic, when the binding lookup fails, or +when substitution would yield another ``T``-ref because the inheriting class forwards the +parameter. ``zend_get_function_declaration`` takes an optional ``subst_ce``: when present, each +printed type runs through ``zend_substitute_proto_type`` so the LSP error message shows the +substituted parent signature rather than the bound-erased form. + +*************************** + Member substitution sites +*************************** + +Substitution is applied at five sites. Each clones the parent's ``zend_property_info`` or +``zend_function``, allocates a fresh ``arg_info`` in the child's arena, and shares body opcodes +with the parent via refcount. Clones keep ``ce`` set to the parent's defining class so +``zend_opcode.c``'s teardown does not double-release shared fields. + +- ``do_inherit_property`` - property type on a class extending a generic parent. +- Property hook signatures - the ``get`` return slot and the ``set`` value-parameter slot. +- ``zend_do_traits_property_binding`` - trait property type, with the using class's binding. +- ``zend_add_trait_method`` - trait method signature, via ``zend_substitute_trait_method_arg_info``. +- ``do_inherit_method`` - non-overridden inherited method signature, via + ``zend_maybe_substitute_inherited_method``. + +Body opcodes are not re-emitted: ``VERIFY_*`` opcodes were laid down at the parent's compile time +against the unsubstituted view, so the child observes the substituted signature on the clone but +the parent's original opcodes inside. + +Diamond detection is in ``zend_validate_generic_diamond_bindings``. It runs *before* ``ce->parent`` is set and +``ZEND_ACC_RESOLVED_PARENT`` is established; the side-table accessors bypass the resolved-parent +gate, so detection sees the bindings directly. For each direct binding source of ce (parent plus +each ``implements`` entry), the check composes the ce-to-source binding with +``zend_get_inheritance_binding_full`` for every generic target reachable from source. Records are +stored in a transient ``HashTable`` keyed by target ``zend_class_entry *``; +``zend_diamond_record_or_check`` compares only arity. Differing arity is structurally +inconsistent and fires ``zend_error_noreturn`` with both source paths. Differing args at matching +arity are admitted at this stage and resolved downstream - the interface-level merge synthesises +a use-site-variance-aware contract on the inheriting interface, and per-path LSP verifies any +concrete implementer against each substituted parent prototype. + +``zend_check_generic_link_bounds`` validates each supplied arg against the corresponding target +parameter's bound. When the supplied arg is a leaf class-scope ``T``-ref of ce, the effective arg +type is ce's own bound on that parameter - "ce's bound on Y must satisfy target's bound on T". An +unbounded child parameter cannot be forwarded into a bounded ancestor slot. + +****************************** + Opcache, optimizer, and JIT +****************************** + +Opcache persistence walks the side tables from ``zend_persist_generic_type_table``: scalar slots +(``return_type``, ``extends``) get duped into SHM via ``zend_shared_memdup_put_free`` and walked +by ``zend_persist_type``; hash slots go through ``zend_persist_generic_type_table_ht``. Calc-side +mirrors. ``zend_persist_attributes`` persists ``zend_attribute->generic_args``. ``zend_persist_type`` +handles NAMED_WITH_ARGS values whose ``name`` is ``NULL``. + +``zend_try_inline_call`` (``Zend/Optimizer/optimize_func_calls.c``) skips its inline transform +when a ``ZEND_VERIFY_GENERIC_ARGUMENTS`` opcode sits between ``INIT_FCALL`` and ``DO_FCALL``: the +verify handler dereferences ``EX(call)->func``, so NOP-ing ``INIT_FCALL`` while leaving the verify +in place would walk an unrelated frame. + +Under JIT, ``ZEND_VERIFY_GENERIC_ARGUMENTS`` falls through to the interpreter. The handler is +cold, so the dispatch cost is dwarfed by the type-comparison work. Code that doesn't use +turbofish doesn't emit the opcode, and the surrounding ``INIT_*`` and ``ZEND_NEW`` JIT +specializations are unaffected. diff --git a/docs/source/index.rst b/docs/source/index.rst index 21e2526f47f6..219264fec13d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,7 @@ :hidden: core/data-structures/index + core/generics .. toctree:: :caption: Miscellaneous diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index af59b9b2c34a..228caf548bfc 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -253,6 +253,12 @@ static void zend_file_cache_serialize_attribute(zval *zv, static void zend_file_cache_unserialize_attribute(zval *zv, zend_persistent_script *script, void *buf); +static void zend_file_cache_serialize_type( + zend_type *type, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf); + +static void zend_file_cache_unserialize_type( + zend_type *type, zend_class_entry *scope, zend_persistent_script *script, void *buf); + static void *zend_file_cache_serialize_interned(zend_string *str, zend_file_cache_metainfo *info) { @@ -468,11 +474,40 @@ static void zend_file_cache_serialize_attribute(zval *zv, SERIALIZE_STR(attr->args[i].name); zend_file_cache_serialize_zval(&attr->args[i].value, script, info, buf); } + + if (attr->generic_args) { + SERIALIZE_PTR(attr->generic_args); + zend_type *t = attr->generic_args; + UNSERIALIZE_PTR(t); + zend_file_cache_serialize_type(t, script, info, buf); + } } static void zend_file_cache_serialize_type( zend_type *type, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*type); + SERIALIZE_PTR(ref); + ZEND_TYPE_SET_PTR(*type, ref); + UNSERIALIZE_PTR(ref); + SERIALIZE_STR(ref->name); + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + SERIALIZE_PTR(named); + ZEND_TYPE_SET_PTR(*type, named); + UNSERIALIZE_PTR(named); + SERIALIZE_STR(named->name); + for (uint32_t i = 0; i < named->count; i++) { + zend_file_cache_serialize_type(&named->args[i], script, info, buf); + } + + return; + } + if (ZEND_TYPE_HAS_LIST(*type)) { zend_type_list *list = ZEND_TYPE_LIST(*type); SERIALIZE_PTR(list); @@ -490,6 +525,82 @@ static void zend_file_cache_serialize_type( } } +static void zend_file_cache_serialize_generic_type_entry( + zval *zv, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + SERIALIZE_PTR(Z_PTR_P(zv)); + zend_type *boxed = Z_PTR_P(zv); + UNSERIALIZE_PTR(boxed); + zend_file_cache_serialize_type(boxed, script, info, buf); +} + +static void zend_file_cache_serialize_generic_type_table_ht( + HashTable **ht_ptr, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + HashTable *ht; + SERIALIZE_PTR(*ht_ptr); + ht = *ht_ptr; + UNSERIALIZE_PTR(ht); + zend_file_cache_serialize_hash(ht, script, info, buf, zend_file_cache_serialize_generic_type_entry); +} + +static void zend_file_cache_serialize_generic_parameter_list( + zend_generic_parameter_list **list_ptr, zend_persistent_script *script, + zend_file_cache_metainfo *info, void *buf) +{ + zend_generic_parameter_list *list; + SERIALIZE_PTR(*list_ptr); + list = *list_ptr; + UNSERIALIZE_PTR(list); + for (uint32_t i = 0; i < list->count; i++) { + SERIALIZE_STR(list->parameters[i].name); + zend_file_cache_serialize_type(&list->parameters[i].bound, script, info, buf); + zend_file_cache_serialize_type(&list->parameters[i].bound_pre_erasure, script, info, buf); + zend_file_cache_serialize_type(&list->parameters[i].default_type, script, info, buf); + zend_file_cache_serialize_type(&list->parameters[i].default_pre_erasure, script, info, buf); + } +} + +static void zend_file_cache_serialize_generic_type_table( + zend_generic_type_table **table_ptr, zend_persistent_script *script, + zend_file_cache_metainfo *info, void *buf) +{ + zend_generic_type_table *table; + SERIALIZE_PTR(*table_ptr); + table = *table_ptr; + UNSERIALIZE_PTR(table); + if (table->return_type) { + SERIALIZE_PTR(table->return_type); + zend_type *t = table->return_type; + UNSERIALIZE_PTR(t); + zend_file_cache_serialize_type(t, script, info, buf); + } + if (table->extends) { + SERIALIZE_PTR(table->extends); + zend_type *t = table->extends; + UNSERIALIZE_PTR(t); + zend_file_cache_serialize_type(t, script, info, buf); + } + if (table->parameters) { + zend_file_cache_serialize_generic_type_table_ht(&table->parameters, script, info, buf); + } + if (table->properties) { + zend_file_cache_serialize_generic_type_table_ht(&table->properties, script, info, buf); + } + if (table->class_constants) { + zend_file_cache_serialize_generic_type_table_ht(&table->class_constants, script, info, buf); + } + if (table->implements) { + zend_file_cache_serialize_generic_type_table_ht(&table->implements, script, info, buf); + } + if (table->trait_uses) { + zend_file_cache_serialize_generic_type_table_ht(&table->trait_uses, script, info, buf); + } + if (table->turbofish_args) { + zend_file_cache_serialize_generic_type_table_ht(&table->turbofish_args, script, info, buf); + } +} + static void zend_file_cache_serialize_op_array(zend_op_array *op_array, zend_persistent_script *script, zend_file_cache_metainfo *info, @@ -700,6 +811,14 @@ static void zend_file_cache_serialize_op_array(zend_op_array *op_arra SERIALIZE_PTR(op_array->try_catch_array); SERIALIZE_PTR(op_array->prototype); SERIALIZE_PTR(op_array->prop_info); + + if (op_array->generic_parameters) { + zend_file_cache_serialize_generic_parameter_list(&op_array->generic_parameters, script, info, buf); + } + + if (op_array->generic_types) { + zend_file_cache_serialize_generic_type_table(&op_array->generic_types, script, info, buf); + } } } @@ -831,6 +950,14 @@ static void zend_file_cache_serialize_class(zval *zv, SERIALIZE_STR(ce->info.user.filename); SERIALIZE_STR(ce->doc_comment); SERIALIZE_ATTRIBUTES(ce->attributes); + if (ce->generic_parameters) { + zend_file_cache_serialize_generic_parameter_list(&ce->generic_parameters, script, info, buf); + } + + if (ce->generic_types) { + zend_file_cache_serialize_generic_type_table(&ce->generic_types, script, info, buf); + } + zend_file_cache_serialize_hash(&ce->properties_info, script, info, buf, zend_file_cache_serialize_prop_info); if (ce->properties_info_table) { @@ -1384,11 +1511,36 @@ static void zend_file_cache_unserialize_attribute(zval *zv, zend_persistent_scri UNSERIALIZE_STR(attr->args[i].name); zend_file_cache_unserialize_zval(&attr->args[i].value, script, buf); } + + if (attr->generic_args) { + UNSERIALIZE_PTR(attr->generic_args); + zend_file_cache_unserialize_type(attr->generic_args, NULL, script, buf); + } } static void zend_file_cache_unserialize_type( zend_type *type, zend_class_entry *scope, zend_persistent_script *script, void *buf) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*type); + UNSERIALIZE_PTR(ref); + ZEND_TYPE_SET_PTR(*type, ref); + UNSERIALIZE_STR(ref->name); + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + UNSERIALIZE_PTR(named); + ZEND_TYPE_SET_PTR(*type, named); + UNSERIALIZE_STR(named->name); + for (uint32_t i = 0; i < named->count; i++) { + zend_file_cache_unserialize_type(&named->args[i], scope, script, buf); + } + + return; + } + if (ZEND_TYPE_HAS_LIST(*type)) { zend_type_list *list = ZEND_TYPE_LIST(*type); UNSERIALIZE_PTR(list); @@ -1410,6 +1562,83 @@ static void zend_file_cache_unserialize_type( } } +static zend_class_entry *zend_file_cache_generic_unserialize_scope = NULL; + +static void zend_file_cache_unserialize_generic_type_entry( + zval *zv, zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(Z_PTR_P(zv)); + zend_file_cache_unserialize_type( + (zend_type *) Z_PTR_P(zv), zend_file_cache_generic_unserialize_scope, script, buf); +} + +static void zend_file_cache_unserialize_generic_type_table_ht( + HashTable **ht_ptr, zend_class_entry *scope, zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*ht_ptr); + HashTable *ht = *ht_ptr; + zend_class_entry *prev = zend_file_cache_generic_unserialize_scope; + zend_file_cache_generic_unserialize_scope = scope; + zend_file_cache_unserialize_hash(ht, script, buf, zend_file_cache_unserialize_generic_type_entry, NULL); + zend_file_cache_generic_unserialize_scope = prev; +} + +static void zend_file_cache_unserialize_generic_parameter_list( + zend_generic_parameter_list **list_ptr, zend_class_entry *scope, + zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*list_ptr); + zend_generic_parameter_list *list = *list_ptr; + for (uint32_t i = 0; i < list->count; i++) { + UNSERIALIZE_STR(list->parameters[i].name); + zend_file_cache_unserialize_type(&list->parameters[i].bound, scope, script, buf); + zend_file_cache_unserialize_type(&list->parameters[i].bound_pre_erasure, scope, script, buf); + zend_file_cache_unserialize_type(&list->parameters[i].default_type, scope, script, buf); + zend_file_cache_unserialize_type(&list->parameters[i].default_pre_erasure, scope, script, buf); + } +} + +static void zend_file_cache_unserialize_generic_type_table( + zend_generic_type_table **table_ptr, zend_class_entry *scope, + zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*table_ptr); + zend_generic_type_table *table = *table_ptr; + if (table->return_type) { + UNSERIALIZE_PTR(table->return_type); + zend_file_cache_unserialize_type(table->return_type, scope, script, buf); + } + + if (table->extends) { + UNSERIALIZE_PTR(table->extends); + zend_file_cache_unserialize_type(table->extends, scope, script, buf); + } + + if (table->parameters) { + zend_file_cache_unserialize_generic_type_table_ht(&table->parameters, scope, script, buf); + } + + if (table->properties) { + zend_file_cache_unserialize_generic_type_table_ht(&table->properties, scope, script, buf); + } + + if (table->class_constants) { + zend_file_cache_unserialize_generic_type_table_ht(&table->class_constants, scope, script, buf); + } + + if (table->implements) { + zend_file_cache_unserialize_generic_type_table_ht(&table->implements, scope, script, buf); + } + + if (table->trait_uses) { + zend_file_cache_unserialize_generic_type_table_ht(&table->trait_uses, scope, script, buf); + } + + if (table->turbofish_args) { + zend_file_cache_unserialize_generic_type_table_ht(&table->turbofish_args, scope, script, buf); + } +} + static void zend_file_cache_unserialize_op_array(zend_op_array *op_array, zend_persistent_script *script, void *buf) @@ -1596,6 +1825,16 @@ static void zend_file_cache_unserialize_op_array(zend_op_array *op_arr UNSERIALIZE_PTR(op_array->try_catch_array); UNSERIALIZE_PTR(op_array->prototype); UNSERIALIZE_PTR(op_array->prop_info); + + if (op_array->generic_parameters) { + zend_file_cache_unserialize_generic_parameter_list(&op_array->generic_parameters, + (op_array->fn_flags & ZEND_ACC_CLOSURE) ? NULL : op_array->scope, script, buf); + } + + if (op_array->generic_types) { + zend_file_cache_unserialize_generic_type_table(&op_array->generic_types, + (op_array->fn_flags & ZEND_ACC_CLOSURE) ? NULL : op_array->scope, script, buf); + } } } @@ -1720,6 +1959,14 @@ static void zend_file_cache_unserialize_class(zval *zv, UNSERIALIZE_STR(ce->info.user.filename); UNSERIALIZE_STR(ce->doc_comment); UNSERIALIZE_ATTRIBUTES(ce->attributes); + if (ce->generic_parameters) { + zend_file_cache_unserialize_generic_parameter_list(&ce->generic_parameters, ce, script, buf); + } + + if (ce->generic_types) { + zend_file_cache_unserialize_generic_type_table(&ce->generic_types, ce, script, buf); + } + zend_file_cache_unserialize_hash(&ce->properties_info, script, buf, zend_file_cache_unserialize_prop_info, NULL); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index c06452e6acf2..d34553a48289 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -85,6 +85,7 @@ typedef void (*zend_persist_func_t)(zval*); static void zend_persist_zval(zval *z); static void zend_persist_op_array(zval *zv); +static void zend_persist_type(zend_type *type); static const uint32_t uninitialized_bucket[-HT_MIN_MASK] = {HT_INVALID_IDX, HT_INVALID_IDX}; @@ -321,6 +322,11 @@ static HashTable *zend_persist_attributes(HashTable *attributes) zend_persist_zval(©->args[i].value); } + if (copy->generic_args) { + copy->generic_args = zend_shared_memdup_put_free(copy->generic_args, sizeof(zend_type)); + zend_persist_type(copy->generic_args); + } + ZVAL_PTR(v, copy); } ZEND_HASH_FOREACH_END(); @@ -359,6 +365,28 @@ uint32_t zend_accel_get_class_name_map_ptr(zend_string *type_name) } static void zend_persist_type(zend_type *type) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*type); + ref = zend_shared_memdup_put_free(ref, sizeof(*ref)); + zend_accel_store_interned_string(ref->name); + ZEND_TYPE_SET_PTR(*type, ref); + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + named = zend_shared_memdup_put_free(named, ZEND_TYPE_NAMED_WITH_ARGS_SIZE(named->count)); + if (named->name) { + zend_accel_store_interned_string(named->name); + } + for (uint32_t i = 0; i < named->count; i++) { + zend_persist_type(&named->args[i]); + } + + ZEND_TYPE_SET_PTR(*type, named); + return; + } + if (ZEND_TYPE_HAS_LIST(*type)) { zend_type_list *list = ZEND_TYPE_LIST(*type); if (ZEND_TYPE_USES_ARENA(*type) || zend_accel_in_shm(list)) { @@ -376,6 +404,17 @@ static void zend_persist_type(zend_type *type) { zend_persist_type(single_type); continue; } + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*single_type)) { + zend_persist_type(single_type); + continue; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*single_type)) { + zend_persist_type(single_type); + continue; + } + if (ZEND_TYPE_HAS_NAME(*single_type)) { zend_string *type_name = ZEND_TYPE_NAME(*single_type); zend_accel_store_interned_string(type_name); @@ -387,6 +426,103 @@ static void zend_persist_type(zend_type *type) { } ZEND_TYPE_FOREACH_END(); } +static zend_generic_parameter_list *zend_persist_generic_parameter_list(zend_generic_parameter_list *list) +{ + if (!list) { + return NULL; + } + + zend_generic_parameter_list *persisted = zend_shared_memdup_put_free( + list, + ZEND_GENERIC_PARAMETER_LIST_SIZE(list->count) + ); + + for (uint32_t i = 0; i < persisted->count; i++) { + zend_generic_parameter *param = &persisted->parameters[i]; + zend_accel_store_interned_string(param->name); + zend_persist_type(¶m->bound); + zend_persist_type(¶m->bound_pre_erasure); + zend_persist_type(¶m->default_type); + zend_persist_type(¶m->default_pre_erasure); + } + + return persisted; +} + +static HashTable *zend_persist_generic_type_table_ht(HashTable *ht) +{ + zend_hash_persist(ht); + if (HT_IS_PACKED(ht)) { + zval *v; + ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { + zend_type *boxed = Z_PTR_P(v); + zend_type *copy = zend_shared_memdup_put_free(boxed, sizeof(zend_type)); + zend_persist_type(copy); + Z_PTR_P(v) = copy; + } ZEND_HASH_FOREACH_END(); + } else { + Bucket *p; + ZEND_HASH_MAP_FOREACH_BUCKET(ht, p) { + if (p->key) { + zend_accel_store_interned_string(p->key); + } + zend_type *boxed = Z_PTR(p->val); + zend_type *copy = zend_shared_memdup_put_free(boxed, sizeof(zend_type)); + zend_persist_type(copy); + Z_PTR(p->val) = copy; + } ZEND_HASH_FOREACH_END(); + } + HashTable *ptr = zend_shared_memdup_put_free(ht, sizeof(HashTable)); + GC_SET_REFCOUNT(ptr, 2); + GC_TYPE_INFO(ptr) = GC_ARRAY | ((IS_ARRAY_IMMUTABLE|GC_NOT_COLLECTABLE) << GC_FLAGS_SHIFT); + return ptr; +} + +static zend_generic_type_table *zend_persist_generic_type_table(zend_generic_type_table *table) +{ + if (!table) { + return NULL; + } + + zend_generic_type_table *persisted = zend_shared_memdup_put_free(table, sizeof(*table)); + persisted->persisted = true; + if (persisted->return_type) { + persisted->return_type = zend_shared_memdup_put_free(persisted->return_type, sizeof(zend_type)); + zend_persist_type(persisted->return_type); + } + + if (persisted->extends) { + persisted->extends = zend_shared_memdup_put_free(persisted->extends, sizeof(zend_type)); + zend_persist_type(persisted->extends); + } + + if (persisted->parameters) { + persisted->parameters = zend_persist_generic_type_table_ht(persisted->parameters); + } + + if (persisted->properties) { + persisted->properties = zend_persist_generic_type_table_ht(persisted->properties); + } + + if (persisted->class_constants) { + persisted->class_constants = zend_persist_generic_type_table_ht(persisted->class_constants); + } + + if (persisted->implements) { + persisted->implements = zend_persist_generic_type_table_ht(persisted->implements); + } + + if (persisted->trait_uses) { + persisted->trait_uses = zend_persist_generic_type_table_ht(persisted->trait_uses); + } + + if (persisted->turbofish_args) { + persisted->turbofish_args = zend_persist_generic_type_table_ht(persisted->turbofish_args); + } + + return persisted; +} + static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_script* main_persistent_script) { zend_op *persist_ptr; @@ -456,8 +592,31 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { arg_info--; } - arg_info = zend_shared_alloc_get_xlat_entry(arg_info); - ZEND_ASSERT(arg_info != NULL); + zend_arg_info *xlat_arg_info = zend_shared_alloc_get_xlat_entry(arg_info); + if (xlat_arg_info) { + arg_info = xlat_arg_info; + } else { + uint32_t num_args = op_array->num_args; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + num_args++; + } + + if (op_array->fn_flags & ZEND_ACC_VARIADIC) { + num_args++; + } + + arg_info = zend_shared_memdup_put(arg_info, sizeof(zend_arg_info) * num_args); + for (uint32_t i = 0; i < num_args; i++) { + if (arg_info[i].name) { + zend_accel_store_interned_string(arg_info[i].name); + } + + zend_persist_type(&arg_info[i].type); + if (arg_info[i].doc_comment) { + zend_accel_store_interned_string(arg_info[i].doc_comment); + } + } + } if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { arg_info++; } @@ -492,6 +651,16 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc op_array->dynamic_func_defs = zend_shared_alloc_get_xlat_entry(op_array->dynamic_func_defs); ZEND_ASSERT(op_array->dynamic_func_defs != NULL); } + if (op_array->generic_parameters) { + zend_generic_parameter_list *xlat_params = zend_shared_alloc_get_xlat_entry(op_array->generic_parameters); + ZEND_ASSERT(xlat_params != NULL); + op_array->generic_parameters = xlat_params; + } + if (op_array->generic_types) { + zend_generic_type_table *xlat_types = zend_shared_alloc_get_xlat_entry(op_array->generic_types); + ZEND_ASSERT(xlat_types != NULL); + op_array->generic_types = xlat_types; + } ZCG(mem) = (void*)((char*)ZCG(mem) + ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist(op_array, ZCG(mem)))); return; } @@ -503,6 +672,47 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc if (op_array->scope && !(op_array->fn_flags & ZEND_ACC_CLOSURE) && (op_array->scope->ce_flags & ZEND_ACC_CACHED)) { + if (op_array->arg_info) { + zend_arg_info *arg_info = op_array->arg_info; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + arg_info--; + } + + if (!zend_accel_in_shm(arg_info)) { + uint32_t num_args = op_array->num_args; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + num_args++; + } + + if (op_array->fn_flags & ZEND_ACC_VARIADIC) { + num_args++; + } + + zend_arg_info *xlat_arg_info = zend_shared_alloc_get_xlat_entry(arg_info); + if (xlat_arg_info) { + arg_info = xlat_arg_info; + } else { + arg_info = zend_shared_memdup_put(arg_info, sizeof(zend_arg_info) * num_args); + for (uint32_t i = 0; i < num_args; i++) { + if (arg_info[i].name) { + zend_accel_store_interned_string(arg_info[i].name); + } + + zend_persist_type(&arg_info[i].type); + if (arg_info[i].doc_comment) { + zend_accel_store_interned_string(arg_info[i].doc_comment); + } + } + } + + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + arg_info++; + } + + op_array->arg_info = arg_info; + } + } + return; } @@ -699,6 +909,14 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc } } + if (op_array->generic_parameters) { + op_array->generic_parameters = zend_persist_generic_parameter_list(op_array->generic_parameters); + } + + if (op_array->generic_types) { + op_array->generic_types = zend_persist_generic_type_table(op_array->generic_types); + } + ZCG(mem) = (void*)((char*)ZCG(mem) + ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist(op_array, ZCG(mem)))); } @@ -867,6 +1085,62 @@ static zend_property_info *zend_persist_property_info(zend_property_info *prop) return prop; } +static zend_property_info *zend_persist_substituted_property_info(zend_property_info *prop) +{ + zend_class_entry *ce; + prop = zend_shared_memdup_put(prop, sizeof(zend_property_info)); + ce = zend_shared_alloc_get_xlat_entry(prop->ce); + if (ce) { + prop->ce = ce; + } + + if (prop->name) { + zend_string *xlat_name = zend_shared_alloc_get_xlat_entry(prop->name); + if (xlat_name) { + prop->name = xlat_name; + } + } + + if (prop->doc_comment) { + zend_string *xlat_doc = zend_shared_alloc_get_xlat_entry(prop->doc_comment); + if (xlat_doc) { + prop->doc_comment = xlat_doc; + } + } + + if (prop->attributes) { + HashTable *xlat_attrs = zend_shared_alloc_get_xlat_entry(prop->attributes); + if (xlat_attrs) { + prop->attributes = xlat_attrs; + } + } + + if (prop->prototype) { + const zend_property_info *xlat_proto = zend_shared_alloc_get_xlat_entry(prop->prototype); + if (xlat_proto) { + prop->prototype = xlat_proto; + } + } + + zend_persist_type(&prop->type); + if (prop->hooks) { + prop->hooks = zend_shared_memdup_put(prop->hooks, ZEND_PROPERTY_HOOK_STRUCT_SIZE); + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop->hooks[i]) { + zend_op_array *hook = zend_persist_class_method(&prop->hooks[i]->op_array, prop->ce); + const zend_property_info *new_prop_info = zend_shared_alloc_get_xlat_entry(hook->prop_info); + if (new_prop_info) { + hook->prop_info = new_prop_info; + } + + prop->hooks[i] = (zend_function *) hook; + } + } + } + + return prop; +} + static void zend_persist_class_constant(zval *zv) { const zend_class_constant *orig_c = Z_PTR_P(zv); @@ -1002,9 +1276,11 @@ zend_class_entry *zend_persist_class_entry(zend_class_entry *orig_ce) if (prop->ce == orig_ce) { Z_PTR(p->val) = zend_persist_property_info(prop); } else { - prop = zend_shared_alloc_get_xlat_entry(prop); - if (prop) { - Z_PTR(p->val) = prop; + zend_property_info *xlat_prop = zend_shared_alloc_get_xlat_entry(prop); + if (xlat_prop) { + Z_PTR(p->val) = xlat_prop; + } else if (prop->flags & ZEND_ACC_GENERIC_CLONE) { + Z_PTR(p->val) = zend_persist_substituted_property_info(prop); } else { /* This can happen if preloading is used and we inherit a property from an * internal class. In that case we should keep pointing to the internal @@ -1066,6 +1342,14 @@ zend_class_entry *zend_persist_class_entry(zend_class_entry *orig_ce) ce->attributes = zend_persist_attributes(ce->attributes); } + if (ce->generic_parameters) { + ce->generic_parameters = zend_persist_generic_parameter_list(ce->generic_parameters); + } + + if (ce->generic_types) { + ce->generic_types = zend_persist_generic_type_table(ce->generic_types); + } + if (ce->num_interfaces && !(ce->ce_flags & ZEND_ACC_LINKED)) { uint32_t i = 0; diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index 9ff37079193b..b894640b5383 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -46,6 +46,7 @@ static void zend_persist_zval_calc(zval *z); static void zend_persist_op_array_calc(const zval *zv); +static void zend_persist_type_calc(zend_type *type); static void zend_hash_persist_calc(const HashTable *ht) { @@ -191,12 +192,37 @@ static void zend_persist_attributes_calc(HashTable *attributes) } zend_persist_zval_calc(&attr->args[i].value); } + + if (attr->generic_args) { + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc(attr->generic_args); + } } ZEND_HASH_FOREACH_END(); } } static void zend_persist_type_calc(zend_type *type) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*type); + ADD_SIZE(sizeof(*ref)); + ADD_INTERNED_STRING(ref->name); + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + ADD_SIZE(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(named->count)); + if (named->name) { + ADD_INTERNED_STRING(named->name); + } + for (uint32_t i = 0; i < named->count; i++) { + zend_persist_type_calc(&named->args[i]); + } + + return; + } + if (ZEND_TYPE_HAS_LIST(*type)) { ADD_SIZE(ZEND_TYPE_LIST_SIZE(ZEND_TYPE_LIST(*type)->num_types)); } @@ -207,6 +233,17 @@ static void zend_persist_type_calc(zend_type *type) zend_persist_type_calc(single_type); continue; } + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*single_type)) { + zend_persist_type_calc(single_type); + continue; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*single_type)) { + zend_persist_type_calc(single_type); + continue; + } + if (ZEND_TYPE_HAS_NAME(*single_type)) { zend_string *type_name = ZEND_TYPE_NAME(*single_type); ADD_INTERNED_STRING(type_name); @@ -215,6 +252,86 @@ static void zend_persist_type_calc(zend_type *type) } ZEND_TYPE_FOREACH_END(); } +static void zend_persist_generic_parameter_list_calc(zend_generic_parameter_list *list) +{ + if (!list) { + return; + } + + ADD_SIZE(ZEND_GENERIC_PARAMETER_LIST_SIZE(list->count)); + for (uint32_t i = 0; i < list->count; i++) { + ADD_INTERNED_STRING(list->parameters[i].name); + zend_persist_type_calc(&list->parameters[i].bound); + zend_persist_type_calc(&list->parameters[i].bound_pre_erasure); + zend_persist_type_calc(&list->parameters[i].default_type); + zend_persist_type_calc(&list->parameters[i].default_pre_erasure); + } +} + +static void zend_persist_generic_type_table_ht_calc(HashTable *ht) +{ + zend_hash_persist_calc(ht); + if (HT_IS_PACKED(ht)) { + zval *v; + ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc((zend_type *) Z_PTR_P(v)); + } ZEND_HASH_FOREACH_END(); + } else { + Bucket *p; + ZEND_HASH_MAP_FOREACH_BUCKET(ht, p) { + if (p->key) { + ADD_INTERNED_STRING(p->key); + } + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc((zend_type *) Z_PTR(p->val)); + } ZEND_HASH_FOREACH_END(); + } + ADD_SIZE(sizeof(HashTable)); +} + +static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) +{ + if (!table) { + return; + } + + ADD_SIZE(sizeof(*table)); + if (table->return_type) { + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc(table->return_type); + } + + if (table->extends) { + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc(table->extends); + } + + if (table->parameters) { + zend_persist_generic_type_table_ht_calc(table->parameters); + } + + if (table->properties) { + zend_persist_generic_type_table_ht_calc(table->properties); + } + + if (table->class_constants) { + zend_persist_generic_type_table_ht_calc(table->class_constants); + } + + if (table->implements) { + zend_persist_generic_type_table_ht_calc(table->implements); + } + + if (table->trait_uses) { + zend_persist_generic_type_table_ht_calc(table->trait_uses); + } + + if (table->turbofish_args) { + zend_persist_generic_type_table_ht_calc(table->turbofish_args); + } +} + static void zend_persist_op_array_calc_ex(zend_op_array *op_array) { if (op_array->function_name) { @@ -231,6 +348,36 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array) if (zend_shared_alloc_get_xlat_entry(op_array->opcodes)) { /* already stored */ ADD_SIZE(ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist_calc(op_array))); + if (op_array->arg_info) { + zend_arg_info *arg_info = op_array->arg_info; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + arg_info--; + } + + if (!zend_shared_alloc_get_xlat_entry(arg_info)) { + uint32_t num_args = op_array->num_args; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + num_args++; + } + + if (op_array->fn_flags & ZEND_ACC_VARIADIC) { + num_args++; + } + + ADD_SIZE(sizeof(zend_arg_info) * num_args); + for (uint32_t i = 0; i < num_args; i++) { + if (arg_info[i].name) { + ADD_INTERNED_STRING(arg_info[i].name); + } + + zend_persist_type_calc(&arg_info[i].type); + if (arg_info[i].doc_comment) { + ADD_INTERNED_STRING(arg_info[i].doc_comment); + } + } + } + } + return; } } @@ -238,6 +385,36 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array) if (op_array->scope && !(op_array->fn_flags & ZEND_ACC_CLOSURE) && (op_array->scope->ce_flags & ZEND_ACC_CACHED)) { + if (op_array->arg_info) { + zend_arg_info *arg_info = op_array->arg_info; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + arg_info--; + } + + if (!zend_accel_in_shm(arg_info) && !zend_shared_alloc_get_xlat_entry(arg_info)) { + uint32_t num_args = op_array->num_args; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + num_args++; + } + + if (op_array->fn_flags & ZEND_ACC_VARIADIC) { + num_args++; + } + + ADD_SIZE(sizeof(zend_arg_info) * num_args); + for (uint32_t i = 0; i < num_args; i++) { + if (arg_info[i].name) { + ADD_INTERNED_STRING(arg_info[i].name); + } + + zend_persist_type_calc(&arg_info[i].type); + if (arg_info[i].doc_comment) { + ADD_INTERNED_STRING(arg_info[i].doc_comment); + } + } + } + } + return; } @@ -344,6 +521,9 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array) } } + zend_persist_generic_parameter_list_calc(op_array->generic_parameters); + zend_persist_generic_type_table_calc(op_array->generic_types); + ADD_SIZE(ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist_calc(op_array))); } @@ -420,6 +600,20 @@ static void zend_persist_property_info_calc(zend_property_info *prop) } } +static void zend_persist_substituted_property_info_calc(zend_property_info *prop) +{ + ADD_SIZE(sizeof(zend_property_info)); + zend_persist_type_calc(&prop->type); + if (prop->hooks) { + ADD_SIZE(ZEND_PROPERTY_HOOK_STRUCT_SIZE); + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop->hooks[i]) { + zend_persist_class_method_calc(&prop->hooks[i]->op_array); + } + } + } +} + static void zend_persist_class_constant_calc(const zval *zv) { zend_class_constant *c = Z_PTR_P(zv); @@ -503,6 +697,9 @@ void zend_persist_class_entry_calc(zend_class_entry *ce) ADD_INTERNED_STRING(p->key); if (prop->ce == ce) { zend_persist_property_info_calc(prop); + } else if ((prop->flags & ZEND_ACC_GENERIC_CLONE) && !zend_shared_alloc_get_xlat_entry(prop)) { + zend_shared_alloc_register_xlat_entry(prop, prop); + zend_persist_substituted_property_info_calc(prop); } } ZEND_HASH_FOREACH_END(); @@ -537,6 +734,9 @@ void zend_persist_class_entry_calc(zend_class_entry *ce) zend_persist_attributes_calc(ce->attributes); } + zend_persist_generic_parameter_list_calc(ce->generic_parameters); + zend_persist_generic_type_table_calc(ce->generic_types); + if (ce->num_interfaces) { uint32_t i; diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index a208741b1590..166c3064eb19 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -45,6 +45,7 @@ #include "zend_closures.h" #include "zend_generators.h" #include "zend_extensions.h" +#include "zend_inheritance.h" #include "zend_builtin_functions.h" #include "zend_smart_str.h" #include "zend_enum.h" @@ -102,6 +103,9 @@ PHPAPI zend_class_entry *reflection_enum_unit_case_ptr; PHPAPI zend_class_entry *reflection_enum_backed_case_ptr; PHPAPI zend_class_entry *reflection_fiber_ptr; PHPAPI zend_class_entry *reflection_constant_ptr; +PHPAPI zend_class_entry *reflection_generic_type_parameter_ptr; +PHPAPI zend_class_entry *reflection_type_parameter_reference_ptr; +PHPAPI zend_class_entry *reflection_generic_variance_ptr; PHPAPI zend_class_entry *reflection_property_hook_type_ptr; #define GET_REFLECTION_OBJECT() do { \ @@ -140,10 +144,132 @@ typedef struct _parameter_reference { /* Struct for type hints */ typedef struct _type_reference { zend_type type; + bool owns_type; /* Whether to use backwards compatible null representation */ bool legacy_behavior; + /* Optional pre-erasure form for generic-aware reflection. */ + zend_type pre_erasure; + bool owns_pre_erasure; + /* Declaring entity for type-parameter resolution. */ + zend_class_entry *declaring_class; + zend_function *declaring_fn; } type_reference; +static zend_type reflection_type_copy(zend_type type); + +static zend_type_list *reflection_type_list_copy(const zend_type_list *old_list) +{ + size_t size = ZEND_TYPE_LIST_SIZE(old_list->num_types); + zend_type_list *new_list = emalloc(size); + new_list->num_types = old_list->num_types; + + for (uint32_t i = 0; i < old_list->num_types; i++) { + new_list->types[i] = reflection_type_copy(old_list->types[i]); + } + + return new_list; +} + +static zend_type_named_with_args *reflection_named_with_args_copy( + const zend_type_named_with_args *old_named) +{ + zend_type_named_with_args *new_named = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(old_named->count)); + new_named->name = old_named->name ? zend_string_copy(old_named->name) : NULL; + new_named->name_attr = old_named->name_attr; + new_named->count = old_named->count; + + for (uint32_t i = 0; i < old_named->count; i++) { + new_named->args[i] = reflection_type_copy(old_named->args[i]); + } + + return new_named; +} + +static zend_type_parameter_ref *reflection_type_parameter_ref_copy( + const zend_type_parameter_ref *old_ref) +{ + zend_type_parameter_ref *new_ref = emalloc(sizeof(zend_type_parameter_ref)); + *new_ref = *old_ref; + if (new_ref->name) { + zend_string_addref(new_ref->name); + } + return new_ref; +} + +static zend_type reflection_type_copy(zend_type type) +{ + zend_type result = type; + + if (ZEND_TYPE_HAS_LIST(type)) { + ZEND_TYPE_SET_LIST(result, reflection_type_list_copy(ZEND_TYPE_LIST(type))); + ZEND_TYPE_FULL_MASK(result) &= ~_ZEND_TYPE_ARENA_BIT; + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + ZEND_TYPE_SET_PTR(result, reflection_named_with_args_copy(ZEND_TYPE_NAMED_WITH_ARGS(type))); + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + ZEND_TYPE_SET_PTR(result, reflection_type_parameter_ref_copy(ZEND_TYPE_TYPE_PARAMETER(type))); + } else if (ZEND_TYPE_HAS_NAME(type)) { + zend_string_addref(ZEND_TYPE_NAME(type)); + } + + return result; +} + +static zend_type reflection_type_substitute_class_params( + zend_type type, const zend_type *args, uint32_t arity) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE && ref->index < arity) { + zend_type result = reflection_type_copy(args[ref->index]); + if (ZEND_TYPE_FULL_MASK(type) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + return result; + } + } + + if (ZEND_TYPE_HAS_LIST(type)) { + zend_type result = type; + const zend_type_list *old_list = ZEND_TYPE_LIST(type); + size_t size = ZEND_TYPE_LIST_SIZE(old_list->num_types); + zend_type_list *new_list = emalloc(size); + new_list->num_types = old_list->num_types; + for (uint32_t i = 0; i < old_list->num_types; i++) { + new_list->types[i] = reflection_type_substitute_class_params( + old_list->types[i], args, arity); + } + ZEND_TYPE_SET_LIST(result, new_list); + ZEND_TYPE_FULL_MASK(result) &= ~_ZEND_TYPE_ARENA_BIT; + return result; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + zend_type result = type; + const zend_type_named_with_args *old_named = ZEND_TYPE_NAMED_WITH_ARGS(type); + zend_type_named_with_args *new_named = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(old_named->count)); + new_named->name = old_named->name ? zend_string_copy(old_named->name) : NULL; + new_named->name_attr = old_named->name_attr; + new_named->count = old_named->count; + for (uint32_t i = 0; i < old_named->count; i++) { + new_named->args[i] = reflection_type_substitute_class_params( + old_named->args[i], args, arity); + } + ZEND_TYPE_SET_PTR(result, new_named); + return result; + } + + return reflection_type_copy(type); +} + +/* Struct for generic type-parameter reflection. Points back at a parameter slot + * inside the declaring entity's zend_generic_parameter_list. Lifetime is bound + * to the declaring class_entry / op_array. */ +typedef struct _generic_parameter_reference { + zend_generic_parameter *param; /* points into a zend_generic_parameter_list */ + uint32_t index; /* position in the list */ + zval declaring; /* zval-wrapped ReflectionClass / ReflectionFunctionAbstract */ +} generic_parameter_reference; + /* Struct for attributes */ typedef struct _attribute_reference { HashTable *attributes; @@ -162,7 +288,8 @@ typedef enum { REF_TYPE_TYPE, REF_TYPE_PROPERTY, REF_TYPE_CLASS_CONSTANT, - REF_TYPE_ATTRIBUTE + REF_TYPE_ATTRIBUTE, + REF_TYPE_GENERIC_PARAMETER } reflection_type_t; /* Struct for reflection objects */ @@ -242,9 +369,14 @@ static void reflection_free_objects_storage(zend_object *object) /* {{{ */ case REF_TYPE_TYPE: { type_reference *type_ref = intern->ptr; - if (ZEND_TYPE_HAS_NAME(type_ref->type)) { + if (type_ref->owns_type) { + zend_type_release(type_ref->type, /* persistent */ false); + } else if (ZEND_TYPE_HAS_NAME(type_ref->type)) { zend_string_release(ZEND_TYPE_NAME(type_ref->type)); } + if (type_ref->owns_pre_erasure) { + zend_type_release(type_ref->pre_erasure, /* persistent */ false); + } efree(type_ref); break; } @@ -262,6 +394,12 @@ static void reflection_free_objects_storage(zend_object *object) /* {{{ */ efree(intern->ptr); break; } + case REF_TYPE_GENERIC_PARAMETER: { + generic_parameter_reference *ref = intern->ptr; + zval_ptr_dtor(&ref->declaring); + efree(ref); + break; + } case REF_TYPE_GENERATOR: case REF_TYPE_FIBER: case REF_TYPE_CLASS_CONSTANT: @@ -1493,11 +1631,69 @@ static reflection_type_kind get_type_kind(zend_type type) { return NAMED_TYPE; } -/* {{{ reflection_type_factory */ -static void reflection_type_factory(zend_type type, zval *object, bool legacy_behavior) +static zend_class_entry *reflection_resolve_declaring_class(zend_class_entry *ce) +{ + return (ce && ce->generic_parameters) ? ce : NULL; +} + +static void reflection_resolve_fn_context( + zend_function *fptr, + zend_class_entry **declaring_class_out, + zend_function **declaring_fn_out) +{ + *declaring_class_out = NULL; + *declaring_fn_out = NULL; + if (fptr->type != ZEND_USER_FUNCTION) { + return; + } + + const zend_op_array *op = &fptr->op_array; + if (op->generic_parameters) { + *declaring_fn_out = fptr; + } + + *declaring_class_out = reflection_resolve_declaring_class(op->scope); +} + +/* {{{ reflection_type_factory_ex */ +static void reflection_type_factory_ex_impl( + zend_type type, zval *object, bool legacy_behavior, + zend_type pre_erasure, + zend_class_entry *declaring_class, + zend_function *declaring_fn, + bool copy_type, + bool copy_pre_erasure) { reflection_object *intern; type_reference *reference; + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + ZEND_ASSERT(!ZEND_TYPE_HAS_NAMED_WITH_ARGS(pre_erasure)); + pre_erasure = type; + copy_pre_erasure = copy_type; + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(type); + type = (zend_type) ZEND_TYPE_INIT_CLASS(named->name, 0, 0); + copy_type = false; + } + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + object_init_ex(object, reflection_type_parameter_reference_ptr); + intern = Z_REFLECTION_P(object); + reference = (type_reference*) emalloc(sizeof(type_reference)); + reference->type = copy_type ? reflection_type_copy(type) : type; + reference->owns_type = copy_type; + reference->legacy_behavior = false; + reference->pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + reference->owns_pre_erasure = false; + reference->declaring_class = declaring_class; + reference->declaring_fn = declaring_fn; + intern->ptr = reference; + intern->ref_type = REF_TYPE_TYPE; + zend_type_parameter_ref *tp = ZEND_TYPE_TYPE_PARAMETER(type); + ZVAL_STR_COPY(reflection_prop_name(object), tp->name); + return; + } + reflection_type_kind type_kind = get_type_kind(type); bool is_mixed = ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY; bool is_only_null = (ZEND_TYPE_PURE_MASK(type) == MAY_BE_NULL && !ZEND_TYPE_IS_COMPLEX(type)); @@ -1517,8 +1713,14 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be intern = Z_REFLECTION_P(object); reference = (type_reference*) emalloc(sizeof(type_reference)); - reference->type = type; + reference->type = copy_type ? reflection_type_copy(type) : type; + reference->owns_type = copy_type; reference->legacy_behavior = legacy_behavior && type_kind == NAMED_TYPE && !is_mixed && !is_only_null; + reference->pre_erasure = copy_pre_erasure && ZEND_TYPE_IS_SET(pre_erasure) + ? reflection_type_copy(pre_erasure) : pre_erasure; + reference->owns_pre_erasure = copy_pre_erasure && ZEND_TYPE_IS_SET(pre_erasure); + reference->declaring_class = declaring_class; + reference->declaring_fn = declaring_fn; intern->ptr = reference; intern->ref_type = REF_TYPE_TYPE; @@ -1527,12 +1729,37 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be * do this for the top-level type, as resolutions inside type lists will be * fully visible to us (we'd have to do a fully copy of the type if we wanted * to prevent that). */ - if (ZEND_TYPE_HAS_NAME(type)) { + if (!copy_type && ZEND_TYPE_HAS_NAME(type)) { zend_string_addref(ZEND_TYPE_NAME(type)); } } + +static void reflection_type_factory_ex( + zend_type type, zval *object, bool legacy_behavior, + zend_type pre_erasure, + zend_class_entry *declaring_class, + zend_function *declaring_fn) +{ + reflection_type_factory_ex_impl(type, object, legacy_behavior, pre_erasure, + declaring_class, declaring_fn, false, false); +} + +static void reflection_type_factory_ex_copy( + zend_type type, zval *object, bool legacy_behavior, + zend_type pre_erasure, + zend_class_entry *declaring_class, + zend_function *declaring_fn) +{ + reflection_type_factory_ex_impl(type, object, legacy_behavior, pre_erasure, + declaring_class, declaring_fn, true, ZEND_TYPE_IS_SET(pre_erasure)); +} /* }}} */ +static void reflection_type_factory(zend_type type, zval *object, bool legacy_behavior) +{ + reflection_type_factory_ex(type, object, legacy_behavior, (zend_type) ZEND_TYPE_INIT_NONE(0), NULL, NULL); +} + /* {{{ reflection_function_factory */ static void reflection_function_factory(zend_function *function, zval *closure_object, zval *object) { @@ -2802,7 +3029,23 @@ ZEND_METHOD(ReflectionParameter, getType) if (!ZEND_TYPE_IS_SET(param->arg_info->type)) { RETURN_NULL(); } - reflection_type_factory(param->arg_info->type, return_value, true); + + zend_type pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + zend_class_entry *declaring_class; + zend_function *declaring_fn; + reflection_resolve_fn_context(param->fptr, &declaring_class, &declaring_fn); + if (param->fptr->type == ZEND_USER_FUNCTION) { + zend_op_array *op = ¶m->fptr->op_array; + if (op->generic_types && op->generic_types->parameters) { + zend_type *boxed = zend_hash_index_find_ptr(op->generic_types->parameters, param->offset); + if (boxed) { + pre_erasure = *boxed; + } + } + } + + reflection_type_factory_ex(param->arg_info->type, return_value, true, + pre_erasure, declaring_class, declaring_fn); } /* }}} */ @@ -3674,7 +3917,19 @@ ZEND_METHOD(ReflectionFunctionAbstract, getReturnType) RETURN_NULL(); } - reflection_type_factory(fptr->common.arg_info[-1].type, return_value, true); + zend_type pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + zend_class_entry *declaring_class; + zend_function *declaring_fn; + reflection_resolve_fn_context(fptr, &declaring_class, &declaring_fn); + if (fptr->type == ZEND_USER_FUNCTION) { + zend_op_array *op = &fptr->op_array; + if (op->generic_types && op->generic_types->return_type) { + pre_erasure = *op->generic_types->return_type; + } + } + + reflection_type_factory_ex(fptr->common.arg_info[-1].type, return_value, true, + pre_erasure, declaring_class, declaring_fn); } /* }}} */ @@ -6422,7 +6677,17 @@ ZEND_METHOD(ReflectionProperty, getType) RETURN_NULL(); } - reflection_type_factory(ref->prop->type, return_value, true); + zend_type pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + zend_class_entry *ce = ref->prop->ce; + if (ce && ce->generic_types && ce->generic_types->properties) { + zend_type *boxed = zend_hash_find_ptr(ce->generic_types->properties, ref->prop->name); + if (boxed) { + pre_erasure = *boxed; + } + } + + reflection_type_factory_ex(ref->prop->type, return_value, true, + pre_erasure, reflection_resolve_declaring_class(ce), NULL); } /* }}} */ @@ -7620,6 +7885,13 @@ ZEND_METHOD(ReflectionAttribute, newInstance) } } + if (attr->data->generic_arity > 0) { + zend_check_generic_new_arguments(ce, attr->data->generic_arity, attr->data->generic_args); + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } + } + zval obj; if (SUCCESS != zend_get_attribute_object(&obj, ce, attr->data, attr->scope, attr->filename)) { @@ -8168,6 +8440,747 @@ ZEND_METHOD(ReflectionConstant, __toString) RETURN_STR(smart_str_extract(&str)); } +ZEND_METHOD(ReflectionGenericTypeParameter, __construct) +{ + zend_throw_exception(reflection_exception_ptr, + "Cannot directly instantiate ReflectionGenericTypeParameter", 0); +} + +ZEND_METHOD(ReflectionTypeParameterReference, __construct) +{ + zend_throw_exception(reflection_exception_ptr, + "Cannot directly instantiate ReflectionTypeParameterReference", 0); +} + + +static void reflection_generic_type_parameter_factory( + zend_generic_parameter *param, uint32_t index, zval *declaring, zval *object) +{ + reflection_object *intern; + generic_parameter_reference *reference; + + object_init_ex(object, reflection_generic_type_parameter_ptr); + intern = Z_REFLECTION_P(object); + reference = emalloc(sizeof(generic_parameter_reference)); + reference->param = param; + reference->index = index; + ZVAL_COPY(&reference->declaring, declaring); + intern->ptr = reference; + intern->ref_type = REF_TYPE_GENERIC_PARAMETER; + + ZVAL_STR_COPY(reflection_prop_name(object), param->name); +} + +static void reflection_build_generic_parameters_array( + zend_generic_parameter_list *list, zval *declaring, zval *return_value) +{ + if (!list) { + array_init(return_value); + return; + } + array_init_size(return_value, list->count); + for (uint32_t i = 0; i < list->count; i++) { + zval entry; + reflection_generic_type_parameter_factory(&list->parameters[i], i, declaring, &entry); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + } +} + +ZEND_METHOD(ReflectionFunctionAbstract, isGeneric) +{ + reflection_object *intern; + zend_function *fptr; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(fptr); + + if (fptr->type != ZEND_USER_FUNCTION) { + RETURN_FALSE; + } + RETURN_BOOL(fptr->op_array.generic_parameters != NULL); +} + +ZEND_METHOD(ReflectionFunctionAbstract, getGenericParameters) +{ + reflection_object *intern; + zend_function *fptr; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(fptr); + + if (fptr->type != ZEND_USER_FUNCTION) { + array_init(return_value); + return; + } + reflection_build_generic_parameters_array(fptr->op_array.generic_parameters, ZEND_THIS, return_value); +} + +ZEND_METHOD(ReflectionClass, isGeneric) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + + RETURN_BOOL(ce->generic_parameters != NULL); +} + +ZEND_METHOD(ReflectionClass, getGenericParameters) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + + reflection_build_generic_parameters_array(ce->generic_parameters, ZEND_THIS, return_value); +} + +#define GET_GENERIC_PARAMETER_REFERENCE(intern, param_ref) \ + do { \ + (intern) = Z_REFLECTION_P(getThis()); \ + if ((intern)->ptr == NULL) { \ + zend_throw_error(NULL, "Internal error: Failed to retrieve the type parameter reference"); \ + RETURN_THROWS(); \ + } \ + (param_ref) = (generic_parameter_reference *) (intern)->ptr; \ + } while (0) + +ZEND_METHOD(ReflectionGenericTypeParameter, getName) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + RETURN_STR_COPY(ref->param->name); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getPosition) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + RETURN_LONG((zend_long) ref->index); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getVariance) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + + const char *case_name; + switch (ref->param->variance) { + case 1: case_name = "Covariant"; break; + case 2: case_name = "Contravariant"; break; + default: case_name = "Invariant"; break; + } + zend_object *case_obj = zend_enum_get_case_cstr(reflection_generic_variance_ptr, case_name); + if (!case_obj) { + zend_throw_error(NULL, "Internal error: ReflectionGenericVariance enum case not found"); + RETURN_THROWS(); + } + ZVAL_OBJ_COPY(return_value, case_obj); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, hasBound) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + RETURN_BOOL(ZEND_TYPE_IS_SET(ref->param->bound)); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getBound) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + + if (!ZEND_TYPE_IS_SET(ref->param->bound)) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "Type parameter %s has no bound", ZSTR_VAL(ref->param->name)); + RETURN_THROWS(); + } + zend_class_entry *declaring_class = NULL; + zend_function *declaring_fn = NULL; + if (Z_TYPE(ref->declaring) == IS_OBJECT) { + zend_object *obj = Z_OBJ(ref->declaring); + if (instanceof_function(obj->ce, reflection_class_ptr)) { + reflection_object *decl_intern = reflection_object_from_obj(obj); + declaring_class = decl_intern->ptr; + } else if (instanceof_function(obj->ce, reflection_function_abstract_ptr)) { + reflection_object *decl_intern = reflection_object_from_obj(obj); + declaring_fn = decl_intern->ptr; + } + } + zend_type primary = ZEND_TYPE_IS_SET(ref->param->bound_pre_erasure) + ? ref->param->bound_pre_erasure : ref->param->bound; + reflection_type_factory_ex(primary, return_value, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), declaring_class, declaring_fn); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, hasDefault) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + RETURN_BOOL(ZEND_TYPE_IS_SET(ref->param->default_type)); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getDefault) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + + if (!ZEND_TYPE_IS_SET(ref->param->default_type)) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "Type parameter %s has no default", ZSTR_VAL(ref->param->name)); + RETURN_THROWS(); + } + + zend_class_entry *declaring_class = NULL; + zend_function *declaring_fn = NULL; + if (Z_TYPE(ref->declaring) == IS_OBJECT) { + zend_object *obj = Z_OBJ(ref->declaring); + if (instanceof_function(obj->ce, reflection_class_ptr)) { + reflection_object *decl_intern = reflection_object_from_obj(obj); + declaring_class = decl_intern->ptr; + } else if (instanceof_function(obj->ce, reflection_function_abstract_ptr)) { + reflection_object *decl_intern = reflection_object_from_obj(obj); + declaring_fn = decl_intern->ptr; + } + } + + zend_type primary = ZEND_TYPE_IS_SET(ref->param->default_pre_erasure) + ? ref->param->default_pre_erasure : ref->param->default_type; + reflection_type_factory_ex(primary, return_value, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), declaring_class, declaring_fn); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getDeclaringEntity) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + ZVAL_COPY(return_value, &ref->declaring); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, __toString) +{ + reflection_object *intern; + generic_parameter_reference *ref; + smart_str str = {0}; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + + switch (ref->param->variance) { + case 1: + smart_str_appendc(&str, '+'); + break; + case 2: + smart_str_appendc(&str, '-'); + break; + default: + break; + } + smart_str_append(&str, ref->param->name); + if (ZEND_TYPE_IS_SET(ref->param->bound)) { + zend_string *bound = zend_type_to_string(ref->param->bound); + smart_str_appends(&str, " : "); + smart_str_append(&str, bound); + zend_string_release(bound); + } + if (ZEND_TYPE_IS_SET(ref->param->default_type)) { + zend_string *def = zend_type_to_string(ref->param->default_type); + smart_str_appends(&str, " = "); + smart_str_append(&str, def); + zend_string_release(def); + } + RETURN_NEW_STR(smart_str_extract(&str)); +} + +ZEND_METHOD(ReflectionTypeParameterReference, getName) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(ref->type)) { + zend_throw_error(NULL, "Type parameter reference has no parameter"); + RETURN_THROWS(); + } + zend_type_parameter_ref *tp = ZEND_TYPE_TYPE_PARAMETER(ref->type); + RETURN_STR_COPY(tp->name); +} + +ZEND_METHOD(ReflectionTypeParameterReference, getTypeParameter) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(ref->type)) { + zend_throw_error(NULL, "Type parameter reference has no parameter"); + RETURN_THROWS(); + } + + zend_type_parameter_ref *tp = ZEND_TYPE_TYPE_PARAMETER(ref->type); + + zend_generic_parameter_list *list = NULL; + zval declaring_zv; + ZVAL_UNDEF(&declaring_zv); + + if (tp->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + if (!ref->declaring_class) { + zend_throw_error(NULL, + "Type parameter reference has no declaring class context"); + RETURN_THROWS(); + } + + list = ref->declaring_class->generic_parameters; + zend_reflection_class_factory(ref->declaring_class, &declaring_zv); + } else { + if (!ref->declaring_fn) { + zend_throw_error(NULL, + "Type parameter reference has no declaring function context"); + RETURN_THROWS(); + } + + list = ref->declaring_fn->op_array.generic_parameters; + if (ref->declaring_fn->op_array.scope) { + reflection_method_factory(ref->declaring_fn->op_array.scope, + ref->declaring_fn, NULL, &declaring_zv); + } else { + reflection_function_factory(ref->declaring_fn, NULL, &declaring_zv); + } + } + + if (!list || tp->index >= list->count) { + zval_ptr_dtor(&declaring_zv); + zend_throw_error(NULL, + "Type parameter index %u out of range for declaring entity", tp->index); + RETURN_THROWS(); + } + + reflection_generic_type_parameter_factory( + &list->parameters[tp->index], tp->index, &declaring_zv, return_value); + zval_ptr_dtor(&declaring_zv); +} + +ZEND_METHOD(ReflectionTypeParameterReference, allowsNull) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_TRUE; +} + +ZEND_METHOD(ReflectionTypeParameterReference, __toString) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(ref->type)) { + RETURN_EMPTY_STRING(); + } + zend_type_parameter_ref *tp = ZEND_TYPE_TYPE_PARAMETER(ref->type); + RETURN_STR_COPY(tp->name); +} + +ZEND_METHOD(ReflectionNamedType, hasGenericArguments) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + RETURN_BOOL(ZEND_TYPE_HAS_NAMED_WITH_ARGS(ref->pre_erasure)); +} + +ZEND_METHOD(ReflectionNamedType, getGenericArguments) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(ref->pre_erasure)) { + array_init(return_value); + return; + } + + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(ref->pre_erasure); + array_init_size(return_value, named->count); + for (uint32_t i = 0; i < named->count; i++) { + zval entry; + reflection_type_factory_ex_copy(named->args[i], &entry, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), + ref->declaring_class, ref->declaring_fn); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + } +} + +static void reflection_build_args_list_ex(zval *return_value, const zend_type *args, + uint32_t count, zend_class_entry *declaring_class, bool copy_types) +{ + array_init_size(return_value, count); + for (uint32_t i = 0; i < count; i++) { + zval entry; + if (copy_types) { + reflection_type_factory_ex_copy(args[i], &entry, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), + declaring_class, NULL); + } else { + reflection_type_factory_ex(args[i], &entry, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), + declaring_class, NULL); + } + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + } +} + +static void reflection_build_args_list(zval *return_value, const zend_type *args, + uint32_t count, zend_class_entry *declaring_class) +{ + reflection_build_args_list_ex(return_value, args, count, declaring_class, false); +} + +static void reflection_build_named_args_list(zval *return_value, const zend_type *boxed, + zend_class_entry *declaring_class) +{ + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + reflection_build_args_list(return_value, named->args, named->count, declaring_class); +} + +static void reflection_type_array_release(zend_type *args, uint32_t arity) +{ + for (uint32_t i = 0; i < arity; i++) { + zend_type_release(args[i], /* persistent */ false); + } +} + +static zend_class_entry *reflection_find_interface_by_name( + zend_class_entry *ce, zend_string *name) +{ + if (!(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES)) { + return NULL; + } + + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + if (ce->interfaces[i] && zend_string_equals_ci(ce->interfaces[i]->name, name)) { + return ce->interfaces[i]; + } + } + + return NULL; +} + +static bool reflection_get_direct_inheritance_binding( + zend_class_entry *ce, zend_class_entry *target, + const zend_type **out_args, uint32_t *out_arity) +{ + if (!ce->generic_types) { + return false; + } + + if (ce->generic_types->extends && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + bool matches = (ce->ce_flags & ZEND_ACC_RESOLVED_PARENT) + ? ce->parent == target + : (named->name && target->name && zend_string_equals_ci(named->name, target->name)); + + if (matches) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } + + if (ce->generic_types->implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + continue; + } + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (named->name && zend_string_equals_ci(named->name, target->name)) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + return false; +} + +static void reflection_append_binding( + zval *return_value, const zend_type *args, uint32_t arity, + const zend_type *ce_args, uint32_t ce_arity, + zend_class_entry *declaring_class) +{ + zval entry; + + if (!ce_args || arity == 0) { + reflection_build_args_list_ex(&entry, args, arity, declaring_class, true); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + return; + } + + ALLOCA_FLAG(use_heap) + zend_type *mapped = (zend_type *) do_alloca(sizeof(zend_type) * arity, use_heap); + for (uint32_t i = 0; i < arity; i++) { + mapped[i] = reflection_type_substitute_class_params(args[i], ce_args, ce_arity); + } + reflection_build_args_list_ex(&entry, mapped, arity, declaring_class, true); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + reflection_type_array_release(mapped, arity); + free_alloca(mapped, use_heap); +} + +static void reflection_collect_interface_bindings( + zval *return_value, zend_class_entry *ce, zend_class_entry *ancestor, + const zend_type *ce_args, uint32_t ce_arity, + zend_class_entry *declaring_class) +{ + if ((ce->ce_flags & ZEND_ACC_RESOLVED_PARENT) && ce->parent && ce->parent != ancestor) { + const zend_type *parent_args; + uint32_t parent_arity; + if (reflection_get_direct_inheritance_binding(ce, ce->parent, &parent_args, &parent_arity)) { + if (ce_args && parent_arity > 0) { + ALLOCA_FLAG(use_heap) + zend_type *mapped = (zend_type *) do_alloca(sizeof(zend_type) * parent_arity, use_heap); + for (uint32_t i = 0; i < parent_arity; i++) { + mapped[i] = reflection_type_substitute_class_params( + parent_args[i], ce_args, ce_arity); + } + reflection_collect_interface_bindings( + return_value, ce->parent, ancestor, mapped, parent_arity, declaring_class); + reflection_type_array_release(mapped, parent_arity); + free_alloca(mapped, use_heap); + } else { + reflection_collect_interface_bindings( + return_value, ce->parent, ancestor, parent_args, parent_arity, declaring_class); + } + } else { + reflection_collect_interface_bindings( + return_value, ce->parent, ancestor, NULL, 0, declaring_class); + } + } + + HashTable *generic_implements = ce->generic_types ? ce->generic_types->implements : NULL; + if (generic_implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(generic_implements, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + continue; + } + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + + if (named->name && zend_string_equals_ci(named->name, ancestor->name)) { + reflection_append_binding(return_value, named->args, named->count, + ce_args, ce_arity, declaring_class); + continue; + } + + zend_class_entry *intermediate = named->name + ? reflection_find_interface_by_name(ce, named->name) : NULL; + if (!intermediate || intermediate == ancestor) { + continue; + } + + if (ce_args && named->count > 0) { + ALLOCA_FLAG(use_heap) + zend_type *mapped = (zend_type *) do_alloca(sizeof(zend_type) * named->count, use_heap); + for (uint32_t i = 0; i < named->count; i++) { + mapped[i] = reflection_type_substitute_class_params( + named->args[i], ce_args, ce_arity); + } + reflection_collect_interface_bindings( + return_value, intermediate, ancestor, mapped, named->count, declaring_class); + reflection_type_array_release(mapped, named->count); + free_alloca(mapped, use_heap); + } else { + reflection_collect_interface_bindings( + return_value, intermediate, ancestor, named->args, named->count, declaring_class); + } + } ZEND_HASH_FOREACH_END(); + } + + if (ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES) { + uint32_t parent_count = ce->parent ? ce->parent->num_interfaces : 0; + for (uint32_t i = parent_count; i < ce->num_interfaces; i++) { + zend_class_entry *intermediate = ce->interfaces[i]; + if (!intermediate || intermediate == ancestor) { + continue; + } + if (generic_implements + && zend_hash_index_exists(generic_implements, i - parent_count)) { + continue; + } + + bool is_transitive = false; + for (uint32_t j = parent_count; j < i && !is_transitive; j++) { + zend_class_entry *earlier = ce->interfaces[j]; + if (!earlier) continue; + for (uint32_t k = 0; k < earlier->num_interfaces; k++) { + if (earlier->interfaces[k] == intermediate) { + is_transitive = true; + break; + } + } + } + if (is_transitive) { + continue; + } + + reflection_collect_interface_bindings( + return_value, intermediate, ancestor, NULL, 0, declaring_class); + } + } +} + +ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentClass) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + + bool has_parent = (ce->ce_flags & ZEND_ACC_LINKED) + ? ce->parent != NULL + : ce->parent_name != NULL; + if (!has_parent) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "Class %s has no parent class", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (ce->generic_types && ce->generic_types->extends) { + reflection_build_named_args_list(return_value, ce->generic_types->extends, ce); + return; + } + RETURN_EMPTY_ARRAY(); +} + +static bool reflection_try_build_named_args_from_table(HashTable *ht, zend_class_entry *ce, zend_string *name, zval *return_value) +{ + if (!ht) { + return false; + } + + zval *zv; + ZEND_HASH_FOREACH_VAL(ht, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (zend_string_equals_ci(named->name, name)) { + reflection_build_named_args_list(return_value, boxed, ce); + return true; + } + } ZEND_HASH_FOREACH_END(); + return false; +} + +ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentInterface) +{ + reflection_object *intern; + zend_class_entry *ce; + zend_string *name; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(name) + ZEND_PARSE_PARAMETERS_END(); + GET_REFLECTION_OBJECT_PTR(ce); + + bool is_ancestor = false; + zend_class_entry *ancestor = NULL; + if (ce->ce_flags & ZEND_ACC_LINKED) { + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + if (zend_string_equals_ci(ce->interfaces[i]->name, name)) { + is_ancestor = true; + ancestor = ce->interfaces[i]; + break; + } + } + } else { + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + if (zend_string_equals_ci(ce->interface_names[i].name, name)) { + is_ancestor = true; + break; + } + } + } + if (!is_ancestor) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "%s is not an ancestor interface of %s", ZSTR_VAL(name), ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + array_init(return_value); + if (ancestor) { + reflection_collect_interface_bindings(return_value, ce, ancestor, NULL, 0, ce); + } +} + +ZEND_METHOD(ReflectionClass, getGenericArgumentsForUsedTrait) +{ + reflection_object *intern; + zend_class_entry *ce; + zend_string *name; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(name) + ZEND_PARSE_PARAMETERS_END(); + GET_REFLECTION_OBJECT_PTR(ce); + + HashTable *ht = ce->generic_types ? ce->generic_types->trait_uses : NULL; + if (reflection_try_build_named_args_from_table(ht, ce, name, return_value)) { + return; + } + + bool is_used = false; + for (uint32_t i = 0; i < ce->num_traits; i++) { + if (zend_string_equals_ci(ce->trait_names[i].name, name)) { + is_used = true; + break; + } + } + if (!is_used) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "%s is not a trait used by %s", ZSTR_VAL(name), ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + RETURN_EMPTY_ARRAY(); +} + PHP_MINIT_FUNCTION(reflection) /* {{{ */ { memcpy(&reflection_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); @@ -8273,6 +9286,16 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */ reflection_property_hook_type_ptr = register_class_PropertyHookType(); + reflection_generic_variance_ptr = register_class_ReflectionGenericVariance(); + + reflection_generic_type_parameter_ptr = register_class_ReflectionGenericTypeParameter(reflector_ptr); + reflection_generic_type_parameter_ptr->create_object = reflection_objects_new; + reflection_generic_type_parameter_ptr->default_object_handlers = &reflection_object_handlers; + + reflection_type_parameter_reference_ptr = register_class_ReflectionTypeParameterReference(reflection_type_ptr); + reflection_type_parameter_reference_ptr->create_object = reflection_objects_new; + reflection_type_parameter_reference_ptr->default_object_handlers = &reflection_object_handlers; + REFLECTION_G(key_initialized) = false; return SUCCESS; diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index dd605100f8ba..f6f7dacb00ed 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -115,6 +115,11 @@ public function hasTentativeReturnType(): bool {} public function getTentativeReturnType(): ?ReflectionType {} public function getAttributes(?string $name = null, int $flags = 0): array {} + + public function isGeneric(): bool {} + + /** @return list */ + public function getGenericParameters(): array {} } class ReflectionFunction extends ReflectionFunctionAbstract @@ -436,6 +441,46 @@ public function getNamespaceName(): string {} public function getShortName(): string {} public function getAttributes(?string $name = null, int $flags = 0): array {} + + public function isGeneric(): bool {} + + /** @return list */ + public function getGenericParameters(): array {} + + /** + * Returns the type arguments this class supplies at the parent-class extends + * site, in source order. Returns an empty array if the extends clause + * specified no type arguments. + * + * @return list + * @throws ReflectionException if this class has no parent class + */ + public function getGenericArgumentsForParentClass(): array {} + + /** + * Returns every generic argument set this class supplies for the named + * ancestor interface, in inheritance traversal order. The outer list + * contains one entry per binding: a class may bind the same generic + * interface multiple times either directly (`implements Foo, + * Foo`) or transitively through different inheritance paths. + * Each inner list holds the type arguments for that binding in source + * order. Returns an empty outer list when no generic bindings to the + * ancestor exist. + * + * @return list> + * @throws ReflectionException if $name is not an ancestor interface + */ + public function getGenericArgumentsForParentInterface(string $name): array {} + + /** + * Returns the type arguments this class supplies at the use site for trait + * $name, in source order. Returns an empty array if no type arguments were + * specified at the use site. + * + * @return list + * @throws ReflectionException if $name is not a directly-used trait + */ + public function getGenericArgumentsForUsedTrait(string $name): array {} } class ReflectionObject extends ReflectionClass @@ -741,6 +786,11 @@ public function getName(): string {} /** @tentative-return-type */ public function isBuiltin(): bool {} + + public function hasGenericArguments(): bool {} + + /** @return list */ + public function getGenericArguments(): array {} } class ReflectionUnionType extends ReflectionType @@ -753,6 +803,66 @@ class ReflectionIntersectionType extends ReflectionType public function getTypes(): array {} } +/** + * @strict-properties + * @not-serializable + */ +final class ReflectionTypeParameterReference extends ReflectionType +{ + public string $name; + + private function __construct() {} + + public function getName(): string {} + + public function getTypeParameter(): ReflectionGenericTypeParameter {} + + public function allowsNull(): bool {} + + public function __toString(): string {} +} + +enum ReflectionGenericVariance +{ + case Invariant; + case Covariant; + case Contravariant; +} + +/** + * @strict-properties + * @not-serializable + */ +final class ReflectionGenericTypeParameter implements Reflector +{ + public string $name; + + private function __construct() {} + + /** @implementation-alias ReflectionClass::__clone */ + private function __clone(): void {} + + public function getName(): string {} + + public function getPosition(): int {} + + public function getVariance(): ReflectionGenericVariance {} + + public function hasBound(): bool {} + + /** @throws ReflectionException if this type parameter has no bound; check hasBound() first */ + public function getBound(): ReflectionType {} + + public function hasDefault(): bool {} + + /** @throws ReflectionException if this type parameter has no default; check hasDefault() first */ + public function getDefault(): ReflectionType {} + + public function getDeclaringEntity(): ReflectionClass|ReflectionFunctionAbstract {} + + public function __toString(): string {} +} + /** @not-serializable */ class ReflectionExtension implements Reflector { diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 65571f38d43c..2f73dbda3866 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: e21013562d5d9f87a8707afdf579f799a858dba3 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) @@ -87,6 +87,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFunctionAbstract ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") ZEND_END_ARG_INFO() +#define arginfo_class_ReflectionFunctionAbstract_isGeneric arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionFunctionAbstract_getGenericParameters arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionFunction___construct, 0, 0, 1) ZEND_ARG_OBJ_TYPE_MASK(0, function, Closure, MAY_BE_STRING, NULL) ZEND_END_ARG_INFO() @@ -367,6 +371,18 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionClass_getAttributes arginfo_class_ReflectionFunctionAbstract_getAttributes +#define arginfo_class_ReflectionClass_isGeneric arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionClass_getGenericParameters arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables + +#define arginfo_class_ReflectionClass_getGenericArgumentsForParentClass arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_getGenericArgumentsForParentInterface, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionClass_getGenericArgumentsForUsedTrait arginfo_class_ReflectionClass_getGenericArgumentsForParentInterface + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionObject___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) ZEND_END_ARG_INFO() @@ -581,10 +597,51 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionNamedType_isBuiltin arginfo_class_ReflectionFunctionAbstract_inNamespace +#define arginfo_class_ReflectionNamedType_hasGenericArguments arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionNamedType_getGenericArguments arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables + #define arginfo_class_ReflectionUnionType_getTypes arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables #define arginfo_class_ReflectionIntersectionType_getTypes arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionTypeParameterReference___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionTypeParameterReference_getName arginfo_class_ReflectionFunction___toString + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ReflectionTypeParameterReference_getTypeParameter, 0, 0, ReflectionGenericTypeParameter, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionTypeParameterReference_allowsNull arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionTypeParameterReference___toString arginfo_class_ReflectionFunction___toString + +#define arginfo_class_ReflectionGenericTypeParameter___construct arginfo_class_ReflectionTypeParameterReference___construct + +#define arginfo_class_ReflectionGenericTypeParameter___clone arginfo_class_ReflectionFunctionAbstract___clone + +#define arginfo_class_ReflectionGenericTypeParameter_getName arginfo_class_ReflectionFunction___toString + +#define arginfo_class_ReflectionGenericTypeParameter_getPosition arginfo_class_ReflectionGenerator_getExecutingLine + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ReflectionGenericTypeParameter_getVariance, 0, 0, ReflectionGenericVariance, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionGenericTypeParameter_hasBound arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ReflectionGenericTypeParameter_getBound, 0, 0, ReflectionType, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionGenericTypeParameter_hasDefault arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionGenericTypeParameter_getDefault arginfo_class_ReflectionGenericTypeParameter_getBound + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_ReflectionGenericTypeParameter_getDeclaringEntity, 0, 0, ReflectionClass|ReflectionFunctionAbstract, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionGenericTypeParameter___toString arginfo_class_ReflectionFunction___toString + #define arginfo_class_ReflectionExtension___clone arginfo_class_ReflectionFunctionAbstract___clone ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionExtension___construct, 0, 0, 1) @@ -641,8 +698,7 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionReference___clone arginfo_class_ReflectionFunctionAbstract___clone -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionReference___construct, 0, 0, 0) -ZEND_END_ARG_INFO() +#define arginfo_class_ReflectionReference___construct arginfo_class_ReflectionTypeParameterReference___construct #define arginfo_class_ReflectionAttribute_getName arginfo_class_ReflectionFunction___toString @@ -659,7 +715,7 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionAttribute___clone arginfo_class_ReflectionFunctionAbstract___clone -#define arginfo_class_ReflectionAttribute___construct arginfo_class_ReflectionReference___construct +#define arginfo_class_ReflectionAttribute___construct arginfo_class_ReflectionTypeParameterReference___construct #define arginfo_class_ReflectionEnum___construct arginfo_class_ReflectionClass___construct @@ -768,6 +824,8 @@ ZEND_METHOD(ReflectionFunctionAbstract, getReturnType); ZEND_METHOD(ReflectionFunctionAbstract, hasTentativeReturnType); ZEND_METHOD(ReflectionFunctionAbstract, getTentativeReturnType); ZEND_METHOD(ReflectionFunctionAbstract, getAttributes); +ZEND_METHOD(ReflectionFunctionAbstract, isGeneric); +ZEND_METHOD(ReflectionFunctionAbstract, getGenericParameters); ZEND_METHOD(ReflectionFunction, __construct); ZEND_METHOD(ReflectionFunction, __toString); ZEND_METHOD(ReflectionFunction, isAnonymous); @@ -863,6 +921,11 @@ ZEND_METHOD(ReflectionClass, inNamespace); ZEND_METHOD(ReflectionClass, getNamespaceName); ZEND_METHOD(ReflectionClass, getShortName); ZEND_METHOD(ReflectionClass, getAttributes); +ZEND_METHOD(ReflectionClass, isGeneric); +ZEND_METHOD(ReflectionClass, getGenericParameters); +ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentClass); +ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentInterface); +ZEND_METHOD(ReflectionClass, getGenericArgumentsForUsedTrait); ZEND_METHOD(ReflectionObject, __construct); ZEND_METHOD(ReflectionProperty, __construct); ZEND_METHOD(ReflectionProperty, __toString); @@ -948,8 +1011,25 @@ ZEND_METHOD(ReflectionType, allowsNull); ZEND_METHOD(ReflectionType, __toString); ZEND_METHOD(ReflectionNamedType, getName); ZEND_METHOD(ReflectionNamedType, isBuiltin); +ZEND_METHOD(ReflectionNamedType, hasGenericArguments); +ZEND_METHOD(ReflectionNamedType, getGenericArguments); ZEND_METHOD(ReflectionUnionType, getTypes); ZEND_METHOD(ReflectionIntersectionType, getTypes); +ZEND_METHOD(ReflectionTypeParameterReference, __construct); +ZEND_METHOD(ReflectionTypeParameterReference, getName); +ZEND_METHOD(ReflectionTypeParameterReference, getTypeParameter); +ZEND_METHOD(ReflectionTypeParameterReference, allowsNull); +ZEND_METHOD(ReflectionTypeParameterReference, __toString); +ZEND_METHOD(ReflectionGenericTypeParameter, __construct); +ZEND_METHOD(ReflectionGenericTypeParameter, getName); +ZEND_METHOD(ReflectionGenericTypeParameter, getPosition); +ZEND_METHOD(ReflectionGenericTypeParameter, getVariance); +ZEND_METHOD(ReflectionGenericTypeParameter, hasBound); +ZEND_METHOD(ReflectionGenericTypeParameter, getBound); +ZEND_METHOD(ReflectionGenericTypeParameter, hasDefault); +ZEND_METHOD(ReflectionGenericTypeParameter, getDefault); +ZEND_METHOD(ReflectionGenericTypeParameter, getDeclaringEntity); +ZEND_METHOD(ReflectionGenericTypeParameter, __toString); ZEND_METHOD(ReflectionExtension, __construct); ZEND_METHOD(ReflectionExtension, __toString); ZEND_METHOD(ReflectionExtension, getName); @@ -1048,6 +1128,8 @@ static const zend_function_entry class_ReflectionFunctionAbstract_methods[] = { ZEND_ME(ReflectionFunctionAbstract, hasTentativeReturnType, arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunctionAbstract, getTentativeReturnType, arginfo_class_ReflectionFunctionAbstract_getTentativeReturnType, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunctionAbstract, getAttributes, arginfo_class_ReflectionFunctionAbstract_getAttributes, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionFunctionAbstract, isGeneric, arginfo_class_ReflectionFunctionAbstract_isGeneric, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionFunctionAbstract, getGenericParameters, arginfo_class_ReflectionFunctionAbstract_getGenericParameters, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -1161,6 +1243,11 @@ static const zend_function_entry class_ReflectionClass_methods[] = { ZEND_ME(ReflectionClass, getNamespaceName, arginfo_class_ReflectionClass_getNamespaceName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, getShortName, arginfo_class_ReflectionClass_getShortName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, getAttributes, arginfo_class_ReflectionClass_getAttributes, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, isGeneric, arginfo_class_ReflectionClass_isGeneric, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, getGenericParameters, arginfo_class_ReflectionClass_getGenericParameters, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, getGenericArgumentsForParentClass, arginfo_class_ReflectionClass_getGenericArgumentsForParentClass, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, getGenericArgumentsForParentInterface, arginfo_class_ReflectionClass_getGenericArgumentsForParentInterface, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, getGenericArgumentsForUsedTrait, arginfo_class_ReflectionClass_getGenericArgumentsForUsedTrait, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -1274,6 +1361,8 @@ static const zend_function_entry class_ReflectionType_methods[] = { static const zend_function_entry class_ReflectionNamedType_methods[] = { ZEND_ME(ReflectionNamedType, getName, arginfo_class_ReflectionNamedType_getName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionNamedType, isBuiltin, arginfo_class_ReflectionNamedType_isBuiltin, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionNamedType, hasGenericArguments, arginfo_class_ReflectionNamedType_hasGenericArguments, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionNamedType, getGenericArguments, arginfo_class_ReflectionNamedType_getGenericArguments, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -1287,6 +1376,30 @@ static const zend_function_entry class_ReflectionIntersectionType_methods[] = { ZEND_FE_END }; +static const zend_function_entry class_ReflectionTypeParameterReference_methods[] = { + ZEND_ME(ReflectionTypeParameterReference, __construct, arginfo_class_ReflectionTypeParameterReference___construct, ZEND_ACC_PRIVATE) + ZEND_ME(ReflectionTypeParameterReference, getName, arginfo_class_ReflectionTypeParameterReference_getName, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionTypeParameterReference, getTypeParameter, arginfo_class_ReflectionTypeParameterReference_getTypeParameter, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionTypeParameterReference, allowsNull, arginfo_class_ReflectionTypeParameterReference_allowsNull, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionTypeParameterReference, __toString, arginfo_class_ReflectionTypeParameterReference___toString, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_ReflectionGenericTypeParameter_methods[] = { + ZEND_ME(ReflectionGenericTypeParameter, __construct, arginfo_class_ReflectionGenericTypeParameter___construct, ZEND_ACC_PRIVATE) + ZEND_RAW_FENTRY("__clone", zim_ReflectionClass___clone, arginfo_class_ReflectionGenericTypeParameter___clone, ZEND_ACC_PRIVATE, NULL, NULL) + ZEND_ME(ReflectionGenericTypeParameter, getName, arginfo_class_ReflectionGenericTypeParameter_getName, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getPosition, arginfo_class_ReflectionGenericTypeParameter_getPosition, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getVariance, arginfo_class_ReflectionGenericTypeParameter_getVariance, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, hasBound, arginfo_class_ReflectionGenericTypeParameter_hasBound, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getBound, arginfo_class_ReflectionGenericTypeParameter_getBound, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, hasDefault, arginfo_class_ReflectionGenericTypeParameter_hasDefault, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getDefault, arginfo_class_ReflectionGenericTypeParameter_getDefault, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getDeclaringEntity, arginfo_class_ReflectionGenericTypeParameter_getDeclaringEntity, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, __toString, arginfo_class_ReflectionGenericTypeParameter___toString, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static const zend_function_entry class_ReflectionExtension_methods[] = { ZEND_RAW_FENTRY("__clone", zim_ReflectionClass___clone, arginfo_class_ReflectionExtension___clone, ZEND_ACC_PRIVATE, NULL, NULL) ZEND_ME(ReflectionExtension, __construct, arginfo_class_ReflectionExtension___construct, ZEND_ACC_PUBLIC) @@ -1808,6 +1921,48 @@ static zend_class_entry *register_class_ReflectionIntersectionType(zend_class_en return class_entry; } +static zend_class_entry *register_class_ReflectionTypeParameterReference(zend_class_entry *class_entry_ReflectionType) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "ReflectionTypeParameterReference", class_ReflectionTypeParameterReference_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_ReflectionType, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + + zval property_name_default_value; + ZVAL_UNDEF(&property_name_default_value); + zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_NAME), &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + + return class_entry; +} + +static zend_class_entry *register_class_ReflectionGenericVariance(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("ReflectionGenericVariance", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Invariant", NULL); + + zend_enum_add_case_cstr(class_entry, "Covariant", NULL); + + zend_enum_add_case_cstr(class_entry, "Contravariant", NULL); + + return class_entry; +} + +static zend_class_entry *register_class_ReflectionGenericTypeParameter(zend_class_entry *class_entry_Reflector) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "ReflectionGenericTypeParameter", class_ReflectionGenericTypeParameter_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Reflector); + + zval property_name_default_value; + ZVAL_UNDEF(&property_name_default_value); + zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_NAME), &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + + return class_entry; +} + static zend_class_entry *register_class_ReflectionExtension(zend_class_entry *class_entry_Reflector) { zend_class_entry ce, *class_entry; diff --git a/ext/reflection/php_reflection_decl.h b/ext/reflection/php_reflection_decl.h index a87e1635419b..43cec2cd9936 100644 --- a/ext/reflection/php_reflection_decl.h +++ b/ext/reflection/php_reflection_decl.h @@ -1,12 +1,18 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 */ + * Stub hash: e21013562d5d9f87a8707afdf579f799a858dba3 */ -#ifndef ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H -#define ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H +#ifndef ZEND_PHP_REFLECTION_DECL_e21013562d5d9f87a8707afdf579f799a858dba3_H +#define ZEND_PHP_REFLECTION_DECL_e21013562d5d9f87a8707afdf579f799a858dba3_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 */ +typedef enum zend_enum_ReflectionGenericVariance { + ZEND_ENUM_ReflectionGenericVariance_Invariant = 1, + ZEND_ENUM_ReflectionGenericVariance_Covariant = 2, + ZEND_ENUM_ReflectionGenericVariance_Contravariant = 3, +} zend_enum_ReflectionGenericVariance; + +#endif /* ZEND_PHP_REFLECTION_DECL_e21013562d5d9f87a8707afdf579f799a858dba3_H */ diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index fd5d83e91741..01415d08aacb 100644 --- a/ext/reflection/tests/ReflectionClass_toString_001.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt @@ -30,7 +30,7 @@ Class [ class ReflectionClass implements Stringable, Refle Property [ public string $name ] } - - Methods [64] { + - Methods [69] { Method [ private method __clone ] { - Parameters [0] { @@ -514,5 +514,42 @@ Class [ class ReflectionClass implements Stringable, Refle } - Return [ array ] } + + Method [ public method isGeneric ] { + + - Parameters [0] { + } + - Return [ bool ] + } + + Method [ public method getGenericParameters ] { + + - Parameters [0] { + } + - Return [ array ] + } + + Method [ public method getGenericArgumentsForParentClass ] { + + - Parameters [0] { + } + - Return [ array ] + } + + Method [ public method getGenericArgumentsForParentInterface ] { + + - Parameters [1] { + Parameter #0 [ string $name ] + } + - Return [ array ] + } + + Method [ public method getGenericArgumentsForUsedTrait ] { + + - Parameters [1] { + Parameter #0 [ string $name ] + } + - Return [ array ] + } } } diff --git a/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt b/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt index 8ba243a503bd..e591c9b2f091 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(29) { ["ReflectionException"]=> object(ReflectionClass)#%d (1) { ["name"]=> @@ -139,4 +139,19 @@ array(26) { ["name"]=> string(16) "PropertyHookType" } + ["ReflectionGenericVariance"]=> + object(ReflectionEnum)#%d (1) { + ["name"]=> + string(25) "ReflectionGenericVariance" + } + ["ReflectionGenericTypeParameter"]=> + object(ReflectionClass)#%d (1) { + ["name"]=> + string(30) "ReflectionGenericTypeParameter" + } + ["ReflectionTypeParameterReference"]=> + object(ReflectionClass)#%d (1) { + ["name"]=> + string(32) "ReflectionTypeParameterReference" + } } diff --git a/ext/reflection/tests/generics/ancestor_args_composite.phpt b/ext/reflection/tests/generics/ancestor_args_composite.phpt new file mode 100644 index 000000000000..dbd45bd29c6a --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_composite.phpt @@ -0,0 +1,19 @@ +--TEST-- +Reflection: composite (union/intersection) type args inside extends/implements +--FILE-- + {} + +class C extends Pair {} + +$args = (new ReflectionClass('C'))->getGenericArgumentsForParentClass(); +echo "count: ", count($args), "\n"; +echo "[0] kind: ", get_class($args[0]), "\n"; +echo "[1] kind: ", get_class($args[1]), "\n"; +?> +--EXPECT-- +count: 2 +[0] kind: ReflectionUnionType +[1] kind: ReflectionIntersectionType diff --git a/ext/reflection/tests/generics/ancestor_args_duplicate_bindings.phpt b/ext/reflection/tests/generics/ancestor_args_duplicate_bindings.phpt new file mode 100644 index 000000000000..0135912d4cf5 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_duplicate_bindings.phpt @@ -0,0 +1,64 @@ +--TEST-- +Reflection: getGenericArgumentsForParentInterface lists every binding when the same interface is bound more than once +--FILE-- + {} +interface Bar {} +interface Baz extends Bar {} + +interface DA<+T> {} +interface I1 extends DA {} +interface I2 extends DA {} + +class Box {} + +class DirectDup implements Foo, Foo {} + +class MixedDup implements Bar, Baz {} + +class ParentDup implements Foo, Foo {} +class ChildDup extends ParentDup {} + +class NestedDup implements Foo>, Foo> {} + +class DiamondVar implements I1, I2 {} + +function render(array $bindings): string { + return '[' . implode(', ', array_map( + static fn(array $b): string => '[' . implode(', ', array_map( + static function (ReflectionType $t): string { + if ($t instanceof ReflectionNamedType && $t->hasGenericArguments()) { + return $t->getName() . '<' . implode(', ', array_map( + static fn(ReflectionType $inner): string => $inner->getName(), + $t->getGenericArguments(), + )) . '>'; + } + return $t->getName(); + }, + $b, + )) . ']', + $bindings, + )) . ']'; +} + +function show(string $cls, string $ancestor): void { + $bindings = (new ReflectionClass($cls))->getGenericArgumentsForParentInterface($ancestor); + printf("%-12s -> %-5s = %s (count=%d)\n", $cls, $ancestor, render($bindings), count($bindings)); +} + +show('DirectDup', 'Foo'); +show('MixedDup', 'Bar'); +show('ChildDup', 'Foo'); +show('NestedDup', 'Foo'); +show('DiamondVar', 'DA'); + +class Single implements Foo {} +show('Single', 'Foo'); +?> +--EXPECT-- +DirectDup -> Foo = [[string], [int]] (count=2) +MixedDup -> Bar = [[string], [int]] (count=2) +ChildDup -> Foo = [[bool], [float]] (count=2) +NestedDup -> Foo = [[Box], [Box]] (count=2) +DiamondVar -> DA = [[int], [string]] (count=2) +Single -> Foo = [[int]] (count=1) diff --git a/ext/reflection/tests/generics/ancestor_args_nested.phpt b/ext/reflection/tests/generics/ancestor_args_nested.phpt new file mode 100644 index 000000000000..0cc893d12a4d --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_nested.phpt @@ -0,0 +1,41 @@ +--TEST-- +Reflection: nested generic arguments inside extends/implements/use are preserved +--FILE-- + {} +class Pair {} +interface I {} +trait T1 { public function noop(): void {} } + +class WithNested + extends Pair, string> + implements I> +{ + use T1>; +} + +$rc = new ReflectionClass('WithNested'); + +// Nested in extends +$args = $rc->getGenericArgumentsForParentClass(); +echo "parent[0]: ", $args[0]->getName(); +echo "<", $args[0]->getGenericArguments()[0]->getName(), ">\n"; +echo "parent[1]: ", $args[1]->getName(), "\n"; + +// Nested in implements +$bindings = $rc->getGenericArgumentsForParentInterface('I'); +echo "I bindings: ", count($bindings), "\n"; +echo "I[0][0]: ", $bindings[0][0]->getName(); +echo "<", $bindings[0][0]->getGenericArguments()[0]->getName(), ">\n"; + +// Nested in use +$args = $rc->getGenericArgumentsForUsedTrait('T1'); +echo "T1[0]: ", $args[0]->getName(); +echo "<", $args[0]->getGenericArguments()[0]->getName(), ">\n"; +?> +--EXPECT-- +parent[0]: Box +parent[1]: string +I bindings: 1 +I[0][0]: Box +T1[0]: Box diff --git a/ext/reflection/tests/generics/ancestor_args_nested_transitive.phpt b/ext/reflection/tests/generics/ancestor_args_nested_transitive.phpt new file mode 100644 index 000000000000..361f93a39019 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_nested_transitive.phpt @@ -0,0 +1,49 @@ +--TEST-- +Reflection: transitive parent interface arguments substitute nested type parameters +--FILE-- + {} +interface Root {} +interface PairRoot {} +interface Mid extends Root> {} +interface Flip extends PairRoot, Box> {} + +class Concrete implements Mid {} +class Forwarded implements Mid {} +class Reordered implements Flip {} + +function show(string $class, string $interface): void { + $bindings = (new ReflectionClass($class))->getGenericArgumentsForParentInterface($interface); + echo "$class/$interface (", count($bindings), " binding)\n"; + foreach ($bindings as $args) { + foreach ($args as $arg) { + echo " ", $arg->getName(); + if ($arg instanceof ReflectionNamedType && $arg->hasGenericArguments()) { + echo "<", implode(", ", array_map( + static fn(ReflectionType $type): string => $type->getName(), + $arg->getGenericArguments(), + )), ">"; + } + echo "\n"; + } + } +} + +show(Concrete::class, Root::class); +show(Forwarded::class, Root::class); +show(Reordered::class, PairRoot::class); + +$forwardedBindings = (new ReflectionClass(Forwarded::class))->getGenericArgumentsForParentInterface(Root::class); +$forwardedInner = $forwardedBindings[0][0]->getGenericArguments()[0]; +echo "Forwarded nested parameter owner: ", + $forwardedInner->getTypeParameter()->getDeclaringEntity()->getName(), "\n"; +?> +--EXPECT-- +Concrete/Root (1 binding) + Box +Forwarded/Root (1 binding) + Box +Reordered/PairRoot (1 binding) + Box + Box +Forwarded nested parameter owner: Forwarded diff --git a/ext/reflection/tests/generics/ancestor_args_no_generics_class.phpt b/ext/reflection/tests/generics/ancestor_args_no_generics_class.phpt new file mode 100644 index 000000000000..b446b8b01789 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_no_generics_class.phpt @@ -0,0 +1,47 @@ +--TEST-- +Reflection: classes with no generic content — empty array when ancestor without args, throw when not an ancestor +--FILE-- +getGenericArgumentsForParentClass(); + echo $cls, ": parent=", json_encode($p); + } catch (ReflectionException $e) { + echo $cls, ": parent=throw(", $e->getMessage(), ")"; + } + + try { + $i = $rc->getGenericArgumentsForParentInterface('IPlain'); + echo " iface=", json_encode($i); + } catch (ReflectionException $e) { + echo " iface=throw(", $e->getMessage(), ")"; + } + + try { + $t = $rc->getGenericArgumentsForUsedTrait('TPlain'); + echo " trait=", json_encode($t); + } catch (ReflectionException $e) { + echo " trait=throw(", $e->getMessage(), ")"; + } + + echo "\n"; +} + +foreach (['Plain', 'WithParent', 'WithIface', 'WithTrait'] as $cls) { + show($cls); +} +?> +--EXPECT-- +Plain: parent=throw(Class Plain has no parent class) iface=throw(IPlain is not an ancestor interface of Plain) trait=throw(TPlain is not a trait used by Plain) +WithParent: parent=[] iface=throw(IPlain is not an ancestor interface of WithParent) trait=throw(TPlain is not a trait used by WithParent) +WithIface: parent=throw(Class WithIface has no parent class) iface=[] trait=throw(TPlain is not a trait used by WithIface) +WithTrait: parent=throw(Class WithTrait has no parent class) iface=throw(IPlain is not an ancestor interface of WithTrait) trait=[] diff --git a/ext/reflection/tests/generics/ancestor_args_parent_class.phpt b/ext/reflection/tests/generics/ancestor_args_parent_class.phpt new file mode 100644 index 000000000000..16a3f48264cf --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_parent_class.phpt @@ -0,0 +1,39 @@ +--TEST-- +Reflection: getGenericArgumentsForParentClass returns args from extends clause, throws when no parent +--FILE-- + {} +class B {} + +class WithArgs extends A {} +class NoArgs extends A {} +class NoParent {} +class Multi extends B {} + +$cases = [ + 'WithArgs' => ['string'], + 'NoArgs' => [], + 'NoParent' => 'throw', + 'Multi' => ['int', 'float'], +]; + +foreach ($cases as $cls => $want) { + try { + $args = (new ReflectionClass($cls))->getGenericArgumentsForParentClass(); + } catch (ReflectionException $e) { + echo $cls, ": ", $want === 'throw' ? "throw OK ({$e->getMessage()})" : "FAIL (unexpected throw)", "\n"; + continue; + } + if ($want === 'throw') { + echo $cls, ": FAIL (expected throw)\n"; + } else { + $got = array_map(fn($t) => $t->getName(), $args); + echo $cls, ": ", $got === $want ? "OK" : ("FAIL got " . implode(",", $got)), "\n"; + } +} +?> +--EXPECT-- +WithArgs: OK +NoArgs: OK +NoParent: throw OK (Class NoParent has no parent class) +Multi: OK diff --git a/ext/reflection/tests/generics/ancestor_args_parent_interface.phpt b/ext/reflection/tests/generics/ancestor_args_parent_interface.phpt new file mode 100644 index 000000000000..7d561cc24f08 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_parent_interface.phpt @@ -0,0 +1,59 @@ +--TEST-- +Reflection: getGenericArgumentsForParentInterface returns every binding to the ancestor interface; throws when not ancestor +--FILE-- + {} +interface J {} +interface K1 extends I {} +interface Forward extends J {} + +class WithArgs implements I {} +class WithoutArgs implements I {} +class NotImplements {} +class Multi implements I, J {} +class ViaK1 implements K1 {} +class ViaForward implements Forward {} +class ChildWithArgs extends WithArgs {} + +function render(array $bindings): string { + return '[' . implode(', ', array_map( + static fn(array $b): string => '[' . implode(',', array_map( + static fn(ReflectionType $t): string => $t->getName(), + $b, + )) . ']', + $bindings, + )) . ']'; +} + +function show(string $cls, string $iface): void { + try { + $bindings = (new ReflectionClass($cls))->getGenericArgumentsForParentInterface($iface); + } catch (ReflectionException $e) { + echo "$cls/$iface: throw ({$e->getMessage()})\n"; + return; + } + echo "$cls/$iface: ", render($bindings), "\n"; +} + +show('WithArgs', 'I'); +show('WithArgs', 'i'); // case insensitive +show('WithoutArgs', 'I'); +show('NotImplements', 'I'); +show('Multi', 'I'); +show('Multi', 'J'); +show('K1', 'I'); +show('ViaK1', 'I'); +show('ViaForward', 'J'); +show('ChildWithArgs', 'I'); +?> +--EXPECT-- +WithArgs/I: [[int,string]] +WithArgs/i: [[int,string]] +WithoutArgs/I: [] +NotImplements/I: throw (I is not an ancestor interface of NotImplements) +Multi/I: [[bool,float]] +Multi/J: [[string]] +K1/I: [[int,string]] +ViaK1/I: [[int,string]] +ViaForward/J: [[bool]] +ChildWithArgs/I: [[int,string]] diff --git a/ext/reflection/tests/generics/ancestor_args_transitive_paths.phpt b/ext/reflection/tests/generics/ancestor_args_transitive_paths.phpt new file mode 100644 index 000000000000..95e4a8839da4 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_transitive_paths.phpt @@ -0,0 +1,77 @@ +--TEST-- +Reflection: getGenericArgumentsForParentInterface composes substitutions through transitive paths and exposes every diamond binding +--FILE-- + {} +interface Mid extends Root {} +interface DeepRoot {} +interface DeepMid extends DeepRoot {} +interface DeepLeaf extends DeepMid {} + +class FwdGeneric implements Mid {} + +class ConcreteThroughGeneric implements Mid {} + +class DeepFwd implements DeepLeaf {} + +class WithIfaceArgs implements Root {} +class ChildOfBound extends WithIfaceArgs {} +class GrandchildOfBound extends ChildOfBound {} + +class ForwardingChild extends WithIfaceArgs {} + +class BaseViaMid implements Mid {} +class ChildViaMid extends BaseViaMid {} + +interface Leaf extends Mid {} + +interface DA<+T> {} +interface I1 extends DA {} +interface I2 extends DA {} +class Diamond implements I1, I2 {} + +function render(array $bindings): string { + return '[' . implode(', ', array_map( + static fn(array $b): string => '[' . implode(', ', array_map( + static fn(ReflectionType $t): string => (string) $t, + $b, + )) . ']', + $bindings, + )) . ']'; +} + +function show(string $cls, string $ancestor): void { + $bindings = (new ReflectionClass($cls))->getGenericArgumentsForParentInterface($ancestor); + printf("%-30s -> %-10s = %s\n", $cls, $ancestor, render($bindings)); +} + +show('FwdGeneric', 'Root'); +show('FwdGeneric', 'Mid'); +show('ConcreteThroughGeneric', 'Root'); +show('DeepFwd', 'DeepRoot'); +show('DeepFwd', 'DeepMid'); +show('DeepFwd', 'DeepLeaf'); +show('ChildOfBound', 'Root'); +show('GrandchildOfBound', 'Root'); +show('ForwardingChild', 'Root'); +show('ChildViaMid', 'Root'); +show('ChildViaMid', 'Mid'); +show('Leaf', 'Root'); +show('Diamond', 'DA'); + +?> +--EXPECT-- +FwdGeneric -> Root = [[U]] +FwdGeneric -> Mid = [[U]] +ConcreteThroughGeneric -> Root = [[int]] +DeepFwd -> DeepRoot = [[U]] +DeepFwd -> DeepMid = [[U]] +DeepFwd -> DeepLeaf = [[U]] +ChildOfBound -> Root = [[int]] +GrandchildOfBound -> Root = [[int]] +ForwardingChild -> Root = [[V]] +ChildViaMid -> Root = [[string]] +ChildViaMid -> Mid = [[string]] +Leaf -> Root = [[bool]] +Diamond -> DA = [[int], [string]] diff --git a/ext/reflection/tests/generics/ancestor_args_type_param_refs.phpt b/ext/reflection/tests/generics/ancestor_args_type_param_refs.phpt new file mode 100644 index 000000000000..c75cdf2a359c --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_type_param_refs.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reflection: type-parameter references inside extends/implements args resolve correctly +--FILE-- + {} +class Holder implements Container {} + +$rc = new ReflectionClass('Holder'); +$bindings = $rc->getGenericArgumentsForParentInterface('Container'); + +echo "bindings: ", count($bindings), "\n"; +echo "args in binding 0: ", count($bindings[0]), "\n"; +echo "class: ", get_class($bindings[0][0]), "\n"; +echo "name: ", $bindings[0][0]->getName(), "\n"; + +$param = $bindings[0][0]->getTypeParameter(); +echo "type param: ", $param->getName(), "\n"; +echo "declaring: ", $param->getDeclaringEntity()->getName(), "\n"; +?> +--EXPECT-- +bindings: 1 +args in binding 0: 1 +class: ReflectionTypeParameterReference +name: T +type param: T +declaring: Holder diff --git a/ext/reflection/tests/generics/ancestor_args_used_trait.phpt b/ext/reflection/tests/generics/ancestor_args_used_trait.phpt new file mode 100644 index 000000000000..322e0066061d --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_used_trait.phpt @@ -0,0 +1,46 @@ +--TEST-- +Reflection: getGenericArgumentsForUsedTrait returns args from use clause; throws when trait not used +--FILE-- + { public T $val; } +trait Pair { public K $k; public V $v; } +trait Plain { public int $x; } + +class WithArgs { use Holder; } +class NoArgs { use Holder; } +class NoTrait {} +class Multi { use Holder, Pair; } +class Combo { use Holder, Plain; } + +function show(string $cls, string $tr): void { + try { + $args = (new ReflectionClass($cls))->getGenericArgumentsForUsedTrait($tr); + } catch (ReflectionException $e) { + echo "$cls/$tr: throw ({$e->getMessage()})\n"; + return; + } + if (!$args) { + echo "$cls/$tr: []\n"; + return; + } + echo "$cls/$tr: ", implode(",", array_map(fn($t)=>$t->getName(), $args)), "\n"; +} + +show('WithArgs', 'Holder'); +show('NoArgs', 'Holder'); +show('NoTrait', 'Holder'); +show('Multi', 'Holder'); +show('Multi', 'Pair'); +show('Combo', 'Holder'); +show('Combo', 'Plain'); +show('Combo', 'holder'); // case insensitive +?> +--EXPECT-- +WithArgs/Holder: string +NoArgs/Holder: [] +NoTrait/Holder: throw (Holder is not a trait used by NoTrait) +Multi/Holder: bool +Multi/Pair: int,float +Combo/Holder: string +Combo/Plain: [] +Combo/holder: string diff --git a/ext/reflection/tests/generics/arrow_fn_get_generic_params.phpt b/ext/reflection/tests/generics/arrow_fn_get_generic_params.phpt new file mode 100644 index 000000000000..829cdaaa8d2f --- /dev/null +++ b/ext/reflection/tests/generics/arrow_fn_get_generic_params.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: getGenericParameters on arrow function +--FILE-- +(T $x): T => $x; +$r = new ReflectionFunction($f); +echo $r->isGeneric() ? "gen\n" : "not\n"; +echo $r->getGenericParameters()[0]->getName(), "\n"; +echo $r->getGenericParameters()[0]->getBound()->getName(), "\n"; +?> +--EXPECT-- +gen +T +int diff --git a/ext/reflection/tests/generics/class_isgeneric_false.phpt b/ext/reflection/tests/generics/class_isgeneric_false.phpt new file mode 100644 index 000000000000..52f5ce8130d1 --- /dev/null +++ b/ext/reflection/tests/generics/class_isgeneric_false.phpt @@ -0,0 +1,9 @@ +--TEST-- +Reflection: ReflectionClass::isGeneric() returns false for non-generic class +--FILE-- +isGeneric()); +?> +--EXPECT-- +bool(false) diff --git a/ext/reflection/tests/generics/class_isgeneric_true.phpt b/ext/reflection/tests/generics/class_isgeneric_true.phpt new file mode 100644 index 000000000000..82eaeaec2247 --- /dev/null +++ b/ext/reflection/tests/generics/class_isgeneric_true.phpt @@ -0,0 +1,9 @@ +--TEST-- +Reflection: ReflectionClass::isGeneric() returns true for generic class +--FILE-- + {} +var_dump((new ReflectionClass('G'))->isGeneric()); +?> +--EXPECT-- +bool(true) diff --git a/ext/reflection/tests/generics/closure_get_generic_params.phpt b/ext/reflection/tests/generics/closure_get_generic_params.phpt new file mode 100644 index 000000000000..8a133191fe72 --- /dev/null +++ b/ext/reflection/tests/generics/closure_get_generic_params.phpt @@ -0,0 +1,16 @@ +--TEST-- +Reflection: getGenericParameters on closure +--FILE-- +(T $x): T { return $x; }; +$r = new ReflectionFunction($cl); +echo $r->isGeneric() ? "gen\n" : "not\n"; +echo count($r->getGenericParameters()), "\n"; +echo $r->getGenericParameters()[0]->getName(), "\n"; +echo $r->getGenericParameters()[0]->getBound()->getName(), "\n"; +?> +--EXPECT-- +gen +1 +T +object diff --git a/ext/reflection/tests/generics/composite_bound.phpt b/ext/reflection/tests/generics/composite_bound.phpt new file mode 100644 index 000000000000..c28ae671d2ad --- /dev/null +++ b/ext/reflection/tests/generics/composite_bound.phpt @@ -0,0 +1,12 @@ +--TEST-- +Reflection: composite (union) bound returned +--FILE-- +(T $x): T { return $x; } +$p = (new ReflectionFunction('f'))->getGenericParameters()[0]; +$b = $p->getBound(); +echo get_class($b), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/ext/reflection/tests/generics/composite_type_arg.phpt b/ext/reflection/tests/generics/composite_type_arg.phpt new file mode 100644 index 000000000000..a482246b9306 --- /dev/null +++ b/ext/reflection/tests/generics/composite_type_arg.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: composite type as generic argument +--FILE-- + {} +function f(Pair $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +$args = $pt->getGenericArguments(); +echo get_class($args[0]), "\n"; +echo get_class($args[1]), "\n"; +?> +--EXPECT-- +ReflectionUnionType +ReflectionIntersectionType diff --git a/ext/reflection/tests/generics/function_isgeneric.phpt b/ext/reflection/tests/generics/function_isgeneric.phpt new file mode 100644 index 000000000000..93998269ea55 --- /dev/null +++ b/ext/reflection/tests/generics/function_isgeneric.phpt @@ -0,0 +1,12 @@ +--TEST-- +Reflection: ReflectionFunction::isGeneric() +--FILE-- +(): void {} +function plain(): void {} +var_dump((new ReflectionFunction('gen'))->isGeneric()); +var_dump((new ReflectionFunction('plain'))->isGeneric()); +?> +--EXPECT-- +bool(true) +bool(false) diff --git a/ext/reflection/tests/generics/get_generic_parameters_count.phpt b/ext/reflection/tests/generics/get_generic_parameters_count.phpt new file mode 100644 index 000000000000..2f9341e4eace --- /dev/null +++ b/ext/reflection/tests/generics/get_generic_parameters_count.phpt @@ -0,0 +1,16 @@ +--TEST-- +Reflection: getGenericParameters returns correct count +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +echo count($ps), "\n"; +echo $ps[0]->getName(), "\n"; +echo $ps[1]->getName(), "\n"; +echo $ps[2]->getName(), "\n"; +?> +--EXPECT-- +3 +X +Y +Z diff --git a/ext/reflection/tests/generics/get_generic_parameters_empty.phpt b/ext/reflection/tests/generics/get_generic_parameters_empty.phpt new file mode 100644 index 000000000000..987c0ef16e15 --- /dev/null +++ b/ext/reflection/tests/generics/get_generic_parameters_empty.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: getGenericParameters on non-generic returns empty array +--FILE-- +getGenericParameters()); +var_dump((new ReflectionFunction('f'))->getGenericParameters()); +?> +--EXPECT-- +array(0) { +} +array(0) { +} diff --git a/ext/reflection/tests/generics/method_get_generic_params.phpt b/ext/reflection/tests/generics/method_get_generic_params.phpt new file mode 100644 index 000000000000..72f019cde580 --- /dev/null +++ b/ext/reflection/tests/generics/method_get_generic_params.phpt @@ -0,0 +1,16 @@ +--TEST-- +Reflection: method type parameters separate from class type parameters +--FILE-- + { + public function f(): void {} +} +$rc = new ReflectionClass('C'); +$class_ps = $rc->getGenericParameters(); +$method_ps = $rc->getMethod('f')->getGenericParameters(); +echo count($class_ps), ": ", $class_ps[0]->getName(), "\n"; +echo count($method_ps), ": ", $method_ps[0]->getName(), "\n"; +?> +--EXPECT-- +1: X +1: U diff --git a/ext/reflection/tests/generics/method_isgeneric.phpt b/ext/reflection/tests/generics/method_isgeneric.phpt new file mode 100644 index 000000000000..ad2014844029 --- /dev/null +++ b/ext/reflection/tests/generics/method_isgeneric.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: ReflectionMethod::isGeneric() +--FILE-- +(): void {} + public function plain(): void {} +} +var_dump((new ReflectionClass('C'))->getMethod('gen')->isGeneric()); +var_dump((new ReflectionClass('C'))->getMethod('plain')->isGeneric()); +?> +--EXPECT-- +bool(true) +bool(false) diff --git a/ext/reflection/tests/generics/named_type_get_name_erased.phpt b/ext/reflection/tests/generics/named_type_get_name_erased.phpt new file mode 100644 index 000000000000..cd89077b1547 --- /dev/null +++ b/ext/reflection/tests/generics/named_type_get_name_erased.phpt @@ -0,0 +1,13 @@ +--TEST-- +Reflection: ReflectionNamedType::getName() returns the erased name +--FILE-- + {} +function f(Box $x): Box { return $x; } +$r = new ReflectionFunction('f'); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Box +Box diff --git a/ext/reflection/tests/generics/named_type_has_args.phpt b/ext/reflection/tests/generics/named_type_has_args.phpt new file mode 100644 index 000000000000..d3e18b456259 --- /dev/null +++ b/ext/reflection/tests/generics/named_type_has_args.phpt @@ -0,0 +1,24 @@ +--TEST-- +Reflection: hasGenericArguments and getGenericArguments +--FILE-- + {} +function f(Box $x): Box { return $x; } +$r = new ReflectionFunction('f'); +$pt = $r->getParameters()[0]->getType(); +var_dump($pt->hasGenericArguments()); +$args = $pt->getGenericArguments(); +echo count($args), "\n"; +echo $args[0]->getName(), "\n"; + +$rt = $r->getReturnType(); +var_dump($rt->hasGenericArguments()); +$args2 = $rt->getGenericArguments(); +echo $args2[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +1 +int +bool(true) +string diff --git a/ext/reflection/tests/generics/named_type_no_args.phpt b/ext/reflection/tests/generics/named_type_no_args.phpt new file mode 100644 index 000000000000..281e29060444 --- /dev/null +++ b/ext/reflection/tests/generics/named_type_no_args.phpt @@ -0,0 +1,12 @@ +--TEST-- +Reflection: hasGenericArguments returns false when no arguments +--FILE-- +getParameters()[0]->getType()->hasGenericArguments()); +var_dump($r->getReturnType()->hasGenericArguments()); +?> +--EXPECT-- +bool(false) +bool(false) diff --git a/ext/reflection/tests/generics/nested_args.phpt b/ext/reflection/tests/generics/nested_args.phpt new file mode 100644 index 000000000000..ad7c352a46bd --- /dev/null +++ b/ext/reflection/tests/generics/nested_args.phpt @@ -0,0 +1,17 @@ +--TEST-- +Reflection: nested generic arguments +--FILE-- + {} +function f(Box> $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +echo $pt->getName(), "\n"; +$inner = $pt->getGenericArguments()[0]; +echo $inner->getName(), "\n"; +$inmost = $inner->getGenericArguments()[0]; +echo $inmost->getName(), "\n"; +?> +--EXPECT-- +Box +Box +int diff --git a/ext/reflection/tests/generics/parameter_constructor_throws.phpt b/ext/reflection/tests/generics/parameter_constructor_throws.phpt new file mode 100644 index 000000000000..d33d9de36190 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_constructor_throws.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: cannot instantiate ReflectionGenericTypeParameter directly +--FILE-- +newInstanceWithoutConstructor()->__construct(); + echo "no error\n"; +} catch (Throwable $e) { + echo "error: ", get_class($e), "\n"; +} +?> +--EXPECT-- +error: ReflectionException diff --git a/ext/reflection/tests/generics/parameter_get_bound.phpt b/ext/reflection/tests/generics/parameter_get_bound.phpt new file mode 100644 index 000000000000..c20c07ac2294 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_bound.phpt @@ -0,0 +1,18 @@ +--TEST-- +Reflection: ReflectionGenericTypeParameter::getBound() returns the bound type or throws when no bound +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +var_dump($ps[0]->hasBound()); +try { + $ps[0]->getBound(); +} catch (ReflectionException $e) { + echo $e->getMessage(), "\n"; +} +echo $ps[1]->getBound()->getName(), "\n"; +?> +--EXPECT-- +bool(false) +Type parameter X has no bound +object diff --git a/ext/reflection/tests/generics/parameter_get_declaring_class.phpt b/ext/reflection/tests/generics/parameter_get_declaring_class.phpt new file mode 100644 index 000000000000..006bb99922d6 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_declaring_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +Reflection: getDeclaringEntity() returns the declaring class +--FILE-- + {} +$p = (new ReflectionClass('A'))->getGenericParameters()[0]; +$de = $p->getDeclaringEntity(); +echo get_class($de), "\n"; +echo $de->getName(), "\n"; +?> +--EXPECT-- +ReflectionClass +A diff --git a/ext/reflection/tests/generics/parameter_get_declaring_function.phpt b/ext/reflection/tests/generics/parameter_get_declaring_function.phpt new file mode 100644 index 000000000000..3ed7cfdc7625 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_declaring_function.phpt @@ -0,0 +1,13 @@ +--TEST-- +Reflection: getDeclaringEntity() returns the declaring function +--FILE-- +(): void {} +$p = (new ReflectionFunction('f'))->getGenericParameters()[0]; +$de = $p->getDeclaringEntity(); +echo get_class($de), "\n"; +echo $de->getName(), "\n"; +?> +--EXPECT-- +ReflectionFunction +f diff --git a/ext/reflection/tests/generics/parameter_get_position.phpt b/ext/reflection/tests/generics/parameter_get_position.phpt new file mode 100644 index 000000000000..babf13acbd9c --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_position.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: ReflectionGenericTypeParameter::getPosition() +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +foreach ($ps as $p) { + echo $p->getName(), ": ", $p->getPosition(), "\n"; +} +?> +--EXPECT-- +X: 0 +Y: 1 +Z: 2 diff --git a/ext/reflection/tests/generics/parameter_get_variance.phpt b/ext/reflection/tests/generics/parameter_get_variance.phpt new file mode 100644 index 000000000000..61d39c1d802c --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_variance.phpt @@ -0,0 +1,13 @@ +--TEST-- +Reflection: ReflectionGenericTypeParameter::getVariance() +--FILE-- + {} +foreach ((new ReflectionClass('A'))->getGenericParameters() as $p) { + echo $p->getName(), ": ", $p->getVariance()->name, "\n"; +} +?> +--EXPECT-- +X: Covariant +Y: Contravariant +Z: Invariant diff --git a/ext/reflection/tests/generics/parameter_has_bound.phpt b/ext/reflection/tests/generics/parameter_has_bound.phpt new file mode 100644 index 000000000000..2e1732073de3 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_has_bound.phpt @@ -0,0 +1,12 @@ +--TEST-- +Reflection: ReflectionGenericTypeParameter::hasBound() +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +echo $ps[0]->getName(), ": ", var_export($ps[0]->hasBound(), true), "\n"; +echo $ps[1]->getName(), ": ", var_export($ps[1]->hasBound(), true), "\n"; +?> +--EXPECT-- +X: false +Y: true diff --git a/ext/reflection/tests/generics/parameter_has_default.phpt b/ext/reflection/tests/generics/parameter_has_default.phpt new file mode 100644 index 000000000000..6e09af852391 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_has_default.phpt @@ -0,0 +1,20 @@ +--TEST-- +Reflection: hasDefault() and getDefault() +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +var_dump($ps[0]->hasDefault()); +try { + $ps[0]->getDefault(); +} catch (ReflectionException $e) { + echo $e->getMessage(), "\n"; +} +var_dump($ps[1]->hasDefault()); +echo $ps[1]->getDefault()->getName(), "\n"; +?> +--EXPECT-- +bool(false) +Type parameter X has no default +bool(true) +int diff --git a/ext/reflection/tests/generics/parameter_to_string.phpt b/ext/reflection/tests/generics/parameter_to_string.phpt new file mode 100644 index 000000000000..60c441816533 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_to_string.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: __toString on type parameter +--FILE-- + {} +foreach ((new ReflectionClass('A'))->getGenericParameters() as $p) { + echo $p, "\n"; +} +?> +--EXPECT-- +X +Y : object +Z = int ++W : object = stdClass diff --git a/ext/reflection/tests/generics/property_type_args.phpt b/ext/reflection/tests/generics/property_type_args.phpt new file mode 100644 index 000000000000..786200f43bfa --- /dev/null +++ b/ext/reflection/tests/generics/property_type_args.phpt @@ -0,0 +1,17 @@ +--TEST-- +Reflection: property type arguments via Reflection +--FILE-- + {} +class Holder { + public Box $b; +} +$rt = (new ReflectionClass('Holder'))->getProperty('b')->getType(); +echo $rt->getName(), "\n"; +echo count($rt->getGenericArguments()), "\n"; +echo $rt->getGenericArguments()[0]->getName(), "\n"; +?> +--EXPECT-- +Box +1 +int diff --git a/ext/reflection/tests/generics/type_parameter_ref_get_param.phpt b/ext/reflection/tests/generics/type_parameter_ref_get_param.phpt new file mode 100644 index 000000000000..642a5b7a79d3 --- /dev/null +++ b/ext/reflection/tests/generics/type_parameter_ref_get_param.phpt @@ -0,0 +1,19 @@ +--TEST-- +Reflection: ReflectionTypeParameterReference::getTypeParameter() +--FILE-- + {} +class Outer { + public Box $b; +} +$rt = (new ReflectionClass('Outer'))->getProperty('b')->getType(); +$ref = $rt->getGenericArguments()[0]; +$param = $ref->getTypeParameter(); +echo get_class($param), "\n"; +echo $param->getName(), "\n"; +echo $param->getDeclaringEntity()->getName(), "\n"; +?> +--EXPECT-- +ReflectionGenericTypeParameter +T +Outer diff --git a/ext/reflection/tests/generics/type_parameter_reference.phpt b/ext/reflection/tests/generics/type_parameter_reference.phpt new file mode 100644 index 000000000000..3ff5ae7d50e4 --- /dev/null +++ b/ext/reflection/tests/generics/type_parameter_reference.phpt @@ -0,0 +1,21 @@ +--TEST-- +Reflection: ReflectionTypeParameterReference appears inside getGenericArguments +--FILE-- + {} +class Outer { + public Box $b; +} +$rp = (new ReflectionClass('Outer'))->getProperty('b'); +$rt = $rp->getType(); +echo $rt->getName(), "\n"; +$args = $rt->getGenericArguments(); +echo count($args), "\n"; +echo get_class($args[0]), "\n"; +echo $args[0]->getName(), "\n"; +?> +--EXPECT-- +Box +1 +ReflectionTypeParameterReference +T diff --git a/ext/reflection/tests/generics/variance_enum.phpt b/ext/reflection/tests/generics/variance_enum.phpt new file mode 100644 index 000000000000..f05241526adb --- /dev/null +++ b/ext/reflection/tests/generics/variance_enum.phpt @@ -0,0 +1,18 @@ +--TEST-- +Reflection: ReflectionGenericVariance is a unit enum exposing three cases +--FILE-- +name, "\n"; +echo ReflectionGenericVariance::Covariant->name, "\n"; +echo ReflectionGenericVariance::Contravariant->name, "\n"; +var_dump(ReflectionGenericVariance::Invariant instanceof BackedEnum); +var_dump(ReflectionGenericVariance::Invariant === ReflectionGenericVariance::Invariant); +var_dump(ReflectionGenericVariance::Invariant === ReflectionGenericVariance::Covariant); +?> +--EXPECT-- +Invariant +Covariant +Contravariant +bool(false) +bool(true) +bool(false) diff --git a/ext/tokenizer/tokenizer_data.c b/ext/tokenizer/tokenizer_data.c index 87b15b8bb345..38f5c5910876 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -166,6 +166,7 @@ char *get_token_type_name(int token_type) case T_DOLLAR_OPEN_CURLY_BRACES: return "T_DOLLAR_OPEN_CURLY_BRACES"; case T_CURLY_OPEN: return "T_CURLY_OPEN"; case T_PAAMAYIM_NEKUDOTAYIM: return "T_DOUBLE_COLON"; + case T_TURBOFISH: return "T_TURBOFISH"; case T_NS_SEPARATOR: return "T_NS_SEPARATOR"; case T_ELLIPSIS: return "T_ELLIPSIS"; case T_COALESCE: return "T_COALESCE"; diff --git a/ext/tokenizer/tokenizer_data.stub.php b/ext/tokenizer/tokenizer_data.stub.php index 57c8edad8acb..e3dffb5e8979 100644 --- a/ext/tokenizer/tokenizer_data.stub.php +++ b/ext/tokenizer/tokenizer_data.stub.php @@ -717,6 +717,11 @@ * @cvalue T_PAAMAYIM_NEKUDOTAYIM */ const T_PAAMAYIM_NEKUDOTAYIM = UNKNOWN; +/** + * @var int + * @cvalue T_TURBOFISH + */ +const T_TURBOFISH = UNKNOWN; /** * @var int * @cvalue T_NS_SEPARATOR diff --git a/ext/tokenizer/tokenizer_data_arginfo.h b/ext/tokenizer/tokenizer_data_arginfo.h index b82842ede0f1..4c5f81240ae5 100644 --- a/ext/tokenizer/tokenizer_data_arginfo.h +++ b/ext/tokenizer/tokenizer_data_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit tokenizer_data.stub.php instead. - * Stub hash: c5235344b7c651d27c2c33c90696a418a9c96837 */ + * Stub hash: 34e6a9cb933770d8434820f2e7b567a05ed95c9e */ static void register_tokenizer_data_symbols(int module_number) { @@ -146,6 +146,7 @@ static void register_tokenizer_data_symbols(int module_number) REGISTER_LONG_CONSTANT("T_DOLLAR_OPEN_CURLY_BRACES", T_DOLLAR_OPEN_CURLY_BRACES, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_CURLY_OPEN", T_CURLY_OPEN, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_PAAMAYIM_NEKUDOTAYIM", T_PAAMAYIM_NEKUDOTAYIM, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_TURBOFISH", T_TURBOFISH, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_NS_SEPARATOR", T_NS_SEPARATOR, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_ELLIPSIS", T_ELLIPSIS, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_COALESCE", T_COALESCE, CONST_PERSISTENT); From 87b40a2af4d2114e25b4507b56a5d05d0ae5e2d1 Mon Sep 17 00:00:00 2001 From: Rob Landers Date: Tue, 19 May 2026 12:43:04 +0200 Subject: [PATCH 02/30] fix deriving concrete type enforcement --- .../method/runtime_check_transitive.phpt | 24 +++++++++++ .../method/runtime_param_check_unbounded.phpt | 38 +++++++++++++++++ .../method/runtime_param_check_variadic.phpt | 29 +++++++++++++ .../method/runtime_param_check_weak_mode.phpt | 30 +++++++++++++ .../runtime_param_check_with_default.phpt | 28 +++++++++++++ .../runtime_return_check_unbounded.phpt | 42 +++++++++++++++++++ Zend/zend_compile.c | 12 +++++- Zend/zend_inheritance.c | 6 +++ Zend/zend_vm_def.h | 16 +++++++ Zend/zend_vm_execute.h | 32 ++++++++++++++ 10 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/generics/inheritance/method/runtime_check_transitive.phpt create mode 100644 Zend/tests/generics/inheritance/method/runtime_param_check_unbounded.phpt create mode 100644 Zend/tests/generics/inheritance/method/runtime_param_check_variadic.phpt create mode 100644 Zend/tests/generics/inheritance/method/runtime_param_check_weak_mode.phpt create mode 100644 Zend/tests/generics/inheritance/method/runtime_param_check_with_default.phpt create mode 100644 Zend/tests/generics/inheritance/method/runtime_return_check_unbounded.phpt diff --git a/Zend/tests/generics/inheritance/method/runtime_check_transitive.phpt b/Zend/tests/generics/inheritance/method/runtime_check_transitive.phpt new file mode 100644 index 000000000000..d468988629b6 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_check_transitive.phpt @@ -0,0 +1,24 @@ +--TEST-- +Inherited methods: runtime check fires across a multi-level generic chain +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class Mid extends Base {} + +class Leaf extends Mid {} + +$l = new Leaf(); +var_dump($l->take(42)); + +try { + $l->take("nope"); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +rejected: Base::take(): Argument #1 ($x) must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_param_check_unbounded.phpt b/Zend/tests/generics/inheritance/method/runtime_param_check_unbounded.phpt new file mode 100644 index 000000000000..a1561e6ec91f --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_param_check_unbounded.phpt @@ -0,0 +1,38 @@ +--TEST-- +Inherited methods: substituted unbounded T parameter is checked at runtime on the child +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class IntBox extends Box {} +class StrBox extends Box {} + +$ib = new IntBox(); +$sb = new StrBox(); + +// Happy paths +var_dump($ib->take(42)); +var_dump($sb->take("hi")); + +// Cross-type rejections +try { + $ib->take("not an int"); +} catch (TypeError $e) { + echo "IntBox rejects string: ", $e->getMessage(), "\n"; +} + +try { + $sb->take(123); +} catch (TypeError $e) { + echo "StrBox rejects int: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(2) "hi" +IntBox rejects string: Box::take(): Argument #1 ($x) must be of type int, string given, called in %s on line %d +StrBox rejects int: Box::take(): Argument #1 ($x) must be of type string, int given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_param_check_variadic.phpt b/Zend/tests/generics/inheritance/method/runtime_param_check_variadic.phpt new file mode 100644 index 000000000000..75858902e2ba --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_param_check_variadic.phpt @@ -0,0 +1,29 @@ +--TEST-- +Inherited methods: substituted variadic T parameter is checked at runtime on the child +--FILE-- + { + public function takeMany(T ...$xs): array { return $xs; } +} + +class IntBox extends Box {} + +$ib = new IntBox(); +var_dump($ib->takeMany(1, 2, 3)); + +try { + $ib->takeMany(1, "two", 3); +} catch (TypeError $e) { + echo "rejects mixed: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +rejects mixed: Box::takeMany(): Argument #2 must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_param_check_weak_mode.phpt b/Zend/tests/generics/inheritance/method/runtime_param_check_weak_mode.phpt new file mode 100644 index 000000000000..b1357e3118d5 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_param_check_weak_mode.phpt @@ -0,0 +1,30 @@ +--TEST-- +Inherited methods: substituted T parameter honors weak-mode coercion like any other type +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class IntBox extends Box {} +class StrBox extends Box {} + +// Numeric strings coerce to int under weak mode +var_dump((new IntBox())->take("42")); + +// Scalars coerce to string under weak mode +var_dump((new StrBox())->take(123)); + +// Non-coercible still throws +try { + (new IntBox())->take("not a number"); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(3) "123" +rejected: Box::take(): Argument #1 ($x) must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_param_check_with_default.phpt b/Zend/tests/generics/inheritance/method/runtime_param_check_with_default.phpt new file mode 100644 index 000000000000..0b87d4c936a2 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_param_check_with_default.phpt @@ -0,0 +1,28 @@ +--TEST-- +Inherited methods: substituted T parameter with a default still rejects bad explicit args +--FILE-- + { + public function take(T $x = null): mixed { return $x; } +} + +class IntBox extends Box {} + +$ib = new IntBox(); + +// Default and matching values pass +var_dump($ib->take()); +var_dump($ib->take(7)); +var_dump($ib->take(null)); + +try { + $ib->take("nope"); +} catch (TypeError $e) { + echo "rejects string: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +NULL +int(7) +NULL +rejects string: Box::take(): Argument #1 ($x) must be of type ?int, string given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_return_check_unbounded.phpt b/Zend/tests/generics/inheritance/method/runtime_return_check_unbounded.phpt new file mode 100644 index 000000000000..be3b761fd686 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_return_check_unbounded.phpt @@ -0,0 +1,42 @@ +--TEST-- +Inherited methods: substituted unbounded T return type is checked at runtime on the child +--FILE-- + { + public mixed $payload = null; + public function get(): T { return $this->payload; } +} + +class IntBox extends Box {} +class StrBox extends Box {} + +$ib = new IntBox(); +$ib->payload = 42; +var_dump($ib->get()); + +$sb = new StrBox(); +$sb->payload = "hi"; +var_dump($sb->get()); + +// Now return values that violate the substituted type +$ib->payload = "not an int"; +try { + $ib->get(); +} catch (TypeError $e) { + echo "IntBox return rejects string: ", $e->getMessage(), "\n"; +} + +$sb->payload = 123; +try { + $sb->get(); +} catch (TypeError $e) { + echo "StrBox return rejects int: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(2) "hi" +IntBox return rejects string: Box::get(): Return value must be of type int, string returned +StrBox return rejects int: Box::get(): Return value must be of type string, int returned diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 9a27d3639277..4d78f783726d 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3393,8 +3393,16 @@ static void zend_emit_return_type_check( } if (expr && ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY) { - /* we don't need run-time check for mixed return type */ - return; + /* Mixed normally needs no run-time check, but if the return is a + * generic parameter that erased to mixed, a child substituting T to + * a concrete type relies on this opcode being present in the shared + * body so its substituted arg_info gets checked. */ + zend_op_array *active = CG(active_op_array); + const zend_type *pre_return = + active->generic_types ? active->generic_types->return_type : NULL; + if (!pre_return || !ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_return)) { + return; + } } if (expr && expr->op_type == IS_CONST && ZEND_TYPE_CONTAINS_CODE(type, Z_TYPE(expr->u.constant))) { diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 08eeef781fcb..35e6d15df661 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -1685,6 +1685,12 @@ static zend_function *zend_maybe_substitute_inherited_method( return NULL; } + /* The clone shares the parent's opcode stream but holds tightened arg_info + * from generic substitution. Mark it like a trait-method clone so opcache + * persistence and the RECV handler can detect that the inline RECV type + * mask may be looser than the live arg_info. */ + clone->common.fn_flags |= ZEND_ACC_TRAIT_CLONE; + function_add_ref(clone); return clone; } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 0271675af476..625590219104 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -5742,6 +5742,14 @@ ZEND_VM_HOT_HANDLER(63, ZEND_RECV, NUM, UNUSED) ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); } + /* The inline op2.num mask was baked in at the origin's compile time. A + * generic-inheritance clone may carry a tighter substituted arg_info that + * the mask doesn't reflect, so force the slow path when the function is a + * clone with substituted types. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + } + ZEND_VM_NEXT_OPCODE(); } @@ -5754,6 +5762,14 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_RECV, op->op2.num == MAY_BE_ANY, ZEND_RECV_NO ZEND_VM_DISPATCH_TO_HELPER(zend_missing_arg_helper); } + /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic + * inheritance clone may carry a substituted type in arg_info that we must + * verify against. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zval *param = EX_VAR(opline->result.var); + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + } + ZEND_VM_NEXT_OPCODE(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 556a3594d710..392cb4af61e5 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -2826,6 +2826,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_R ZEND_VM_TAIL_CALL(zend_missing_arg_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); } + /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic + * inheritance clone may carry a substituted type in arg_info that we must + * verify against. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zval *param = EX_VAR(opline->result.var); + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } + ZEND_VM_NEXT_OPCODE(); } @@ -4341,6 +4349,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_R ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); } + /* The inline op2.num mask was baked in at the origin's compile time. A + * generic-inheritance clone may carry a tighter substituted arg_info that + * the mask doesn't reflect, so force the slow path when the function is a + * clone with substituted types. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } + ZEND_VM_NEXT_OPCODE(); } @@ -55679,6 +55695,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_RECV_N ZEND_VM_TAIL_CALL(zend_missing_arg_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); } + /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic + * inheritance clone may carry a substituted type in arg_info that we must + * verify against. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zval *param = EX_VAR(opline->result.var); + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } + ZEND_VM_NEXT_OPCODE(); } @@ -57100,6 +57124,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_RECV_S ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); } + /* The inline op2.num mask was baked in at the origin's compile time. A + * generic-inheritance clone may carry a tighter substituted arg_info that + * the mask doesn't reflect, so force the slow path when the function is a + * clone with substituted types. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } + ZEND_VM_NEXT_OPCODE(); } From 69ca60d8215a9b9ad23c8c3d9c733d34943e1efa Mon Sep 17 00:00:00 2001 From: Rob Landers Date: Tue, 19 May 2026 17:10:36 +0200 Subject: [PATCH 03/30] monomorphize class generics --- .../extends_with_mutual_bounds.phpt | 2 +- .../runtime_instantiation.phpt | 4 +- .../variance/function_happy_path.phpt | 2 +- .../mixed_class_T_and_function_T.phpt | 2 +- .../declaration/variance/rfc_example.phpt | 4 +- .../erasure/extends_args_discarded.phpt | 4 +- .../erasure/get_class_returns_erased.phpt | 4 +- .../erasure/t_in_nullable_and_union.phpt | 2 +- .../generics/erasure/var_dump_class_name.phpt | 4 +- .../errors/naked_new_without_defaults.phpt | 9 + .../instanceof_monomorph_parent.phpt | 46 + .../property_hook_on_inherited_T.phpt | 39 + .../recursive_bounds_extends.phpt | 37 + .../variance_through_synthesis.phpt | 38 + .../implements_args/implements_monomorph.phpt | 61 ++ .../trait_use_args/trait_monomorph.phpt | 64 ++ .../runtime/dynamic_new_synthesis.phpt | 46 + .../runtime/lsb_through_monomorph.phpt | 59 ++ .../runtime/monomorph_canonical_unions.phpt | 44 + .../runtime/monomorph_class_exists.phpt | 25 + .../monomorph_dnf_canonicalization.phpt | 50 + .../runtime/monomorph_dynamic_new.phpt | 34 + .../runtime/monomorph_nested_args.phpt | 37 + .../runtime/monomorph_observables_sweep.phpt | 100 ++ .../runtime/monomorph_opcache_file_cache.phpt | 35 + .../runtime/monomorph_reflection.phpt | 32 + .../runtime/monomorph_reflection_surface.phpt | 58 ++ .../generics/runtime/monomorph_serialize.phpt | 29 + .../monomorph_typed_class_constants.phpt | 36 + .../naked_new_self_no_defaults_lenient.phpt | 27 + .../runtime/naked_new_self_static_parent.phpt | 40 + .../runtime/static_property_isolation.phpt | 47 + Zend/tests/generics/syntax/turbofish_new.phpt | 2 +- .../traits/abstract_method_violates.phpt | 2 +- .../generics/turbofish/arity/new_arity.phpt | 6 +- .../turbofish/naked_new_with_defaults.phpt | 29 + Zend/zend_compile.c | 332 ++++++- Zend/zend_compile.h | 15 + Zend/zend_execute_API.c | 20 + Zend/zend_inheritance.c | 854 +++++++++++++++++- Zend/zend_inheritance.h | 41 + Zend/zend_vm_def.h | 49 + Zend/zend_vm_execute.h | 260 ++++++ ext/reflection/php_reflection.c | 19 + 44 files changed, 2621 insertions(+), 29 deletions(-) create mode 100644 Zend/tests/generics/errors/naked_new_without_defaults.phpt create mode 100644 Zend/tests/generics/inheritance/extends_args/instanceof_monomorph_parent.phpt create mode 100644 Zend/tests/generics/inheritance/extends_args/property_hook_on_inherited_T.phpt create mode 100644 Zend/tests/generics/inheritance/extends_args/recursive_bounds_extends.phpt create mode 100644 Zend/tests/generics/inheritance/extends_args/variance_through_synthesis.phpt create mode 100644 Zend/tests/generics/inheritance/implements_args/implements_monomorph.phpt create mode 100644 Zend/tests/generics/inheritance/trait_use_args/trait_monomorph.phpt create mode 100644 Zend/tests/generics/runtime/dynamic_new_synthesis.phpt create mode 100644 Zend/tests/generics/runtime/lsb_through_monomorph.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_canonical_unions.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_class_exists.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_dnf_canonicalization.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_dynamic_new.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_nested_args.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_observables_sweep.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_opcache_file_cache.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_reflection.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_reflection_surface.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_serialize.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_typed_class_constants.phpt create mode 100644 Zend/tests/generics/runtime/naked_new_self_no_defaults_lenient.phpt create mode 100644 Zend/tests/generics/runtime/naked_new_self_static_parent.phpt create mode 100644 Zend/tests/generics/runtime/static_property_isolation.phpt create mode 100644 Zend/tests/generics/turbofish/naked_new_with_defaults.phpt diff --git a/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt b/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt index 67b2f02ac727..f3b2c515d3f6 100644 --- a/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt +++ b/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt @@ -2,7 +2,7 @@ Recursive bounds: a child class can extend a parent that uses mutual bounds --FILE-- {} +class Box {} class Pair, U: Box> { public function __construct(public Box $left, public Box $right) {} } diff --git a/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt b/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt index db3d865da3b9..53c310f5f629 100644 --- a/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt +++ b/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt @@ -2,7 +2,7 @@ Recursive bounds: a class with mutually recursive bounds can be instantiated and used --FILE-- { +class Box { public function __construct(public mixed $value) {} } @@ -12,7 +12,7 @@ final class Pair, U: Box> { $bx = new Box(1); $by = new Box("hi"); -$p = new Pair($bx, $by); +$p = new Pair::, Box>($bx, $by); var_dump($p->left->value, $p->right->value); ?> --EXPECT-- diff --git a/Zend/tests/generics/declaration/variance/function_happy_path.phpt b/Zend/tests/generics/declaration/variance/function_happy_path.phpt index ccd63d67e2ba..5640a800ccb7 100644 --- a/Zend/tests/generics/declaration/variance/function_happy_path.phpt +++ b/Zend/tests/generics/declaration/variance/function_happy_path.phpt @@ -11,7 +11,7 @@ $closure_make = function <+T>(): T { return null; }; $arrow_take = fn<-T>(T $x): int => 0; $arrow_make = fn<+T>(): T => null; -class A<+T> { +class A<+T = mixed> { public function map<-A_, +B>(A_ $a): B { return null; } } diff --git a/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt b/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt index fbc0d827d949..3e1b9bc8c0fa 100644 --- a/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt +++ b/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt @@ -2,7 +2,7 @@ Variance: class-T and function-T variance markers are checked independently in the same method --FILE-- { +class C<+T = mixed> { public function map<-A, +B>(A $a): B { return null; } } diff --git a/Zend/tests/generics/declaration/variance/rfc_example.phpt b/Zend/tests/generics/declaration/variance/rfc_example.phpt index 29aa6bfff6ba..b34e65f25a6e 100644 --- a/Zend/tests/generics/declaration/variance/rfc_example.phpt +++ b/Zend/tests/generics/declaration/variance/rfc_example.phpt @@ -3,7 +3,7 @@ RFC example: covariant Pair/Box with self-referential return type, zip, map, ide --FILE-- { +final readonly class Pair<+L = mixed, +R = mixed> { public function __construct( public L $left, public R $right, @@ -14,7 +14,7 @@ final readonly class Pair<+L, +R> { } } -final readonly class Box<+T> { +final readonly class Box<+T = mixed> { public function __construct( public T $value, ) {} diff --git a/Zend/tests/generics/erasure/extends_args_discarded.phpt b/Zend/tests/generics/erasure/extends_args_discarded.phpt index 9fb1c7f59c72..1c46d8103850 100644 --- a/Zend/tests/generics/erasure/extends_args_discarded.phpt +++ b/Zend/tests/generics/erasure/extends_args_discarded.phpt @@ -1,5 +1,5 @@ --TEST-- -Erasure: extends type arguments don't change runtime parent +Inheritance: `extends Base` makes the direct parent the synthesized monomorph --FILE-- {} @@ -9,5 +9,5 @@ $d = new Derived; var_dump($d instanceof Base); ?> --EXPECT-- -Base +Base bool(true) diff --git a/Zend/tests/generics/erasure/get_class_returns_erased.phpt b/Zend/tests/generics/erasure/get_class_returns_erased.phpt index 51e27f3d54a6..2a85a21c3e31 100644 --- a/Zend/tests/generics/erasure/get_class_returns_erased.phpt +++ b/Zend/tests/generics/erasure/get_class_returns_erased.phpt @@ -1,5 +1,5 @@ --TEST-- -Erasure: get_class returns the erased class name +Monomorphization: get_class returns the canonical monomorph name --FILE-- {} @@ -7,4 +7,4 @@ $b = new Box::; echo get_class($b), "\n"; ?> --EXPECT-- -Box +Box diff --git a/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt b/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt index 10456f3cfe9f..91f3959bce95 100644 --- a/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt +++ b/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt @@ -9,7 +9,7 @@ function unionMultiUnbounded(int|bool|T $x): int|bool|T { return $x; } function unionNullBounded(null|T $x): null|T { return $x; } function unionMultiBounded(int|T $x): int|T { return $x; } -class Holder { +class Holder { public function __construct(public ?T $value) {} public function set(?T $v): void { $this->value = $v; } public function get(): ?T { return $this->value; } diff --git a/Zend/tests/generics/erasure/var_dump_class_name.phpt b/Zend/tests/generics/erasure/var_dump_class_name.phpt index c852fe9d88ee..5852b3910887 100644 --- a/Zend/tests/generics/erasure/var_dump_class_name.phpt +++ b/Zend/tests/generics/erasure/var_dump_class_name.phpt @@ -1,5 +1,5 @@ --TEST-- -Erasure: var_dump shows erased class name +Monomorphization: var_dump shows the canonical monomorph name --FILE-- { @@ -8,7 +8,7 @@ class Box { var_dump(new Box::); ?> --EXPECT-- -object(Box)#1 (1) { +object(Box)#1 (1) { ["x"]=> int(1) } diff --git a/Zend/tests/generics/errors/naked_new_without_defaults.phpt b/Zend/tests/generics/errors/naked_new_without_defaults.phpt new file mode 100644 index 000000000000..290ebd8f38bd --- /dev/null +++ b/Zend/tests/generics/errors/naked_new_without_defaults.phpt @@ -0,0 +1,9 @@ +--TEST-- +Errors: naked `new GenericClass()` is rejected if any type parameter has no default +--FILE-- + {} +new Box(); +?> +--EXPECTF-- +Fatal error: Cannot instantiate generic class Box without type arguments; type parameter T has no default in %s on line %d diff --git a/Zend/tests/generics/inheritance/extends_args/instanceof_monomorph_parent.phpt b/Zend/tests/generics/inheritance/extends_args/instanceof_monomorph_parent.phpt new file mode 100644 index 000000000000..710f785f0ed8 --- /dev/null +++ b/Zend/tests/generics/inheritance/extends_args/instanceof_monomorph_parent.phpt @@ -0,0 +1,46 @@ +--TEST-- +Extends-with-args: subclass's direct parent is the canonical monomorph +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class IntBox extends Box {} +class StrBox extends Box {} + +$i = new IntBox(42); +$s = new StrBox("hi"); + +// instanceof at the AST level discards type arguments, so we verify the +// monomorph parent via reflection and string class comparisons instead. +var_dump($i instanceof Box); +var_dump($i instanceof IntBox); + +$rcInt = new ReflectionClass(IntBox::class); +$rcStr = new ReflectionClass(StrBox::class); +var_dump($rcInt->getParentClass()->getName()); +var_dump($rcStr->getParentClass()->getName()); + +// Each subclass's direct parent is its own distinct monomorph. +var_dump($rcInt->getParentClass()->getName() !== $rcStr->getParentClass()->getName()); + +// The grandparent is the bare base, shared. +var_dump($rcInt->getParentClass()->getParentClass()->getName()); +var_dump($rcInt->getParentClass()->getParentClass()->getName() + === $rcStr->getParentClass()->getParentClass()->getName()); + +// And the canonical monomorphs are registered with their canonical names. +var_dump(class_exists("Box", false)); +var_dump(class_exists("Box", false)); +?> +--EXPECT-- +bool(true) +bool(true) +string(8) "Box" +string(11) "Box" +bool(true) +string(3) "Box" +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/extends_args/property_hook_on_inherited_T.phpt b/Zend/tests/generics/inheritance/extends_args/property_hook_on_inherited_T.phpt new file mode 100644 index 000000000000..8b694703ac69 --- /dev/null +++ b/Zend/tests/generics/inheritance/extends_args/property_hook_on_inherited_T.phpt @@ -0,0 +1,39 @@ +--TEST-- +Extends-with-args: property hook on inherited T-typed property survives monomorph link +--FILE-- + { + public T $value { + get => $this->value; + set(T $v) { $this->value = $v; } + } + public function __construct(T $v) { $this->value = $v; } +} + +class IntBox extends Box {} + +$b = new IntBox(42); +echo "type: ", (new ReflectionProperty($b, "value"))->getType(), "\n"; +echo "value: ", $b->value, "\n"; + +// Set hook with the substituted type accepts an int. +$b->value = 99; +echo "after set: ", $b->value, "\n"; + +// And the inherited typed property is monomorph-substituted to int. +$rc = new ReflectionClass($b); +echo "parent: ", $rc->getParentClass()->getName(), "\n"; + +$baseRC = new ReflectionClass(Box::class); +echo "base property type: ", $baseRC->getProperty('value')->getType(), "\n"; + +$monoRC = new ReflectionClass("Box"); +echo "mono property type: ", $monoRC->getProperty('value')->getType(), "\n"; +?> +--EXPECT-- +type: int +value: 42 +after set: 99 +parent: Box +base property type: mixed +mono property type: int diff --git a/Zend/tests/generics/inheritance/extends_args/recursive_bounds_extends.phpt b/Zend/tests/generics/inheritance/extends_args/recursive_bounds_extends.phpt new file mode 100644 index 000000000000..0930b9b017cb --- /dev/null +++ b/Zend/tests/generics/inheritance/extends_args/recursive_bounds_extends.phpt @@ -0,0 +1,37 @@ +--TEST-- +Extends-with-args: subclass with recursive-bound type parameters extending a generic parent +--FILE-- + {} + +class Pair, U: Box> { + public function __construct(public Box $left, public Box $right) {} +} + +// Subclass binds Pair's parameters to two concrete monomorphs. +class StrictPair, B: Box> extends Pair {} + +// Two further subclasses, both passing concrete args to StrictPair. +class IntFirst extends StrictPair, Box> {} +class StrFirst extends StrictPair, Box> {} + +$p1 = new IntFirst(new Box(), new Box()); +$p2 = new StrFirst(new Box(), new Box()); + +echo (new ReflectionClass($p1))->getParentClass()->getName(), "\n"; +echo (new ReflectionClass($p2))->getParentClass()->getName(), "\n"; + +// Distinct StrictPair monos. +var_dump((new ReflectionClass($p1))->getParentClass()->getName() + !== (new ReflectionClass($p2))->getParentClass()->getName()); + +// Both transitively extend Pair (bare base, two hops up). +var_dump($p1 instanceof Pair); +var_dump($p2 instanceof Pair); +?> +--EXPECT-- +StrictPair,Box> +StrictPair,Box> +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/extends_args/variance_through_synthesis.phpt b/Zend/tests/generics/inheritance/extends_args/variance_through_synthesis.phpt new file mode 100644 index 000000000000..b2657361e619 --- /dev/null +++ b/Zend/tests/generics/inheritance/extends_args/variance_through_synthesis.phpt @@ -0,0 +1,38 @@ +--TEST-- +Variance enforcement at synthesis: extends-with-args composes through the monomorph +--FILE-- + { + public function get(): T { return new Animal(); } +} + +class DogBox extends Box {} +$d = new DogBox(); +var_dump($d instanceof Box); +var_dump((new ReflectionClass($d))->getParentClass()->getName()); + +// Two synthesized monos extending the same base both succeed; their own +// concrete signatures honour covariance. +class AnimalBox extends Box {} +$rcAnimalMono = new ReflectionClass("Box"); +echo "Box::get returns: ", $rcAnimalMono->getMethod('get')->getReturnType(), "\n"; +$rcDogMono = new ReflectionClass("Box"); +echo "Box::get returns: ", $rcDogMono->getMethod('get')->getReturnType(), "\n"; + +// Bound violation at extends-with-args fires the standard error. +try { + eval('class IntBox extends Box {}'); +} catch (\Throwable $e) { + echo "bound violation: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +bool(true) +string(8) "Box" +Box::get returns: Animal +Box::get returns: Dog +%aType argument 1 to extends Box in IntBox does not satisfy the bound Animal on parameter T, int given%a diff --git a/Zend/tests/generics/inheritance/implements_args/implements_monomorph.phpt b/Zend/tests/generics/inheritance/implements_args/implements_monomorph.phpt new file mode 100644 index 000000000000..7af42f043164 --- /dev/null +++ b/Zend/tests/generics/inheritance/implements_args/implements_monomorph.phpt @@ -0,0 +1,61 @@ +--TEST-- +Implements-with-args: implementing class's direct interface is the canonical monomorph +--FILE-- + { + public function next(): T; +} + +class IntIter implements Iter { + public function __construct(public int $v) {} + public function next(): int { return ++$this->v; } +} + +class StrIter implements Iter { + public function next(): string { return "a"; } +} + +$i = new IntIter(0); + +// The monomorph is recorded as the directly-implemented interface; the bare +// base shows up transitively. +$rcInt = new ReflectionClass(IntIter::class); +$names = $rcInt->getInterfaceNames(); +sort($names); +var_dump($names); + +$rcStr = new ReflectionClass(StrIter::class); +$names = $rcStr->getInterfaceNames(); +sort($names); +var_dump($names); + +// instanceof against the bare base works transitively. +var_dump($i instanceof Iter); + +// The monomorph and its base are registered. +var_dump(interface_exists("Iter", false)); +var_dump(interface_exists("Iter", false)); +var_dump(interface_exists("Iter", false)); + +// The monomorph itself reports the correct kind. +$rcMono = new ReflectionClass("Iter"); +var_dump($rcMono->isInterface()); +?> +--EXPECT-- +array(2) { + [0]=> + string(4) "Iter" + [1]=> + string(9) "Iter" +} +array(2) { + [0]=> + string(4) "Iter" + [1]=> + string(12) "Iter" +} +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/trait_use_args/trait_monomorph.phpt b/Zend/tests/generics/inheritance/trait_use_args/trait_monomorph.phpt new file mode 100644 index 000000000000..a81e43d381ae --- /dev/null +++ b/Zend/tests/generics/inheritance/trait_use_args/trait_monomorph.phpt @@ -0,0 +1,64 @@ +--TEST-- +Trait-use-with-args: each `use Foo;` binds to the canonical monomorph trait +--FILE-- + { + public T $value; + public function get(): T { return $this->value; } + public function set(T $v): void { $this->value = $v; } +} + +class IntHolder { + use Box; +} + +class StrHolder { + use Box; +} + +$i = new IntHolder(); +$i->set(42); +var_dump($i->get()); + +$s = new StrHolder(); +$s->set("hi"); +var_dump($s->get()); + +// Substituted property type is visible per use-site. +$rcI = new ReflectionClass(IntHolder::class); +var_dump($rcI->getTraitNames()); +echo "IntHolder::value type: ", $rcI->getProperty('value')->getType(), "\n"; + +$rcS = new ReflectionClass(StrHolder::class); +var_dump($rcS->getTraitNames()); +echo "StrHolder::value type: ", $rcS->getProperty('value')->getType(), "\n"; + +// The trait monomorphs are registered as traits. +var_dump(trait_exists("Box", false)); +var_dump(trait_exists("Box", false)); +var_dump((new ReflectionClass("Box"))->isTrait()); + +// Wrong-type set throws TypeError on the substituted signature. +try { + $i->set("not int"); +} catch (TypeError $e) { + echo "ok: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(2) "hi" +array(1) { + [0]=> + string(8) "Box" +} +IntHolder::value type: int +array(1) { + [0]=> + string(11) "Box" +} +StrHolder::value type: string +bool(true) +bool(true) +bool(true) +ok: %sset()%sint%s diff --git a/Zend/tests/generics/runtime/dynamic_new_synthesis.phpt b/Zend/tests/generics/runtime/dynamic_new_synthesis.phpt new file mode 100644 index 000000000000..952680530dc1 --- /dev/null +++ b/Zend/tests/generics/runtime/dynamic_new_synthesis.phpt @@ -0,0 +1,46 @@ +--TEST-- +Dynamic `new $name()`: bare generic with defaults synthesizes; without defaults throws +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class NoDefault { + public function __construct(public mixed $value) {} +} + +// Dynamic new of a bare generic with defaults → defaults monomorph. +$cls = 'Box'; +$b = new $cls(42); +var_dump($b::class); + +// Dynamic new of a canonical name → that monomorph. +$cls2 = 'Box'; +$b2 = new $cls2("x"); +var_dump($b2::class); + +// Dynamic new with turbofish: the explicit args win. +$cls3 = 'Box'; +$b3 = new $cls3::(1.5); +var_dump($b3::class); + +// Dynamic new of a no-defaults bare generic → Error. +try { + $cls4 = 'NoDefault'; + new $cls4("oops"); +} catch (Error $e) { + echo "err: ", $e->getMessage(), "\n"; +} + +// But turbofish on the same bare name works. +$cls5 = 'NoDefault'; +$b5 = new $cls5::(7); +var_dump($b5::class); +?> +--EXPECT-- +string(8) "Box" +string(11) "Box" +string(10) "Box" +err: Cannot instantiate generic class NoDefault without type arguments via dynamic class name; no defaults declared +string(14) "NoDefault" diff --git a/Zend/tests/generics/runtime/lsb_through_monomorph.phpt b/Zend/tests/generics/runtime/lsb_through_monomorph.phpt new file mode 100644 index 000000000000..39b0d2c31e5b --- /dev/null +++ b/Zend/tests/generics/runtime/lsb_through_monomorph.phpt @@ -0,0 +1,59 @@ +--TEST-- +Late static binding from inside a monomorph: static::class / static::$prop / static::FOO +--FILE-- + { + public static int $count = 0; + const TAG = "Box"; + + public static function whoAmI(): string { return static::class; } + public static function bump(): void { static::$count++; } + public static function tag(): string { return static::TAG; } + public static function self_(): static { return new static(0); } + + public function __construct(public mixed $value) {} +} + +class StrBox extends Box { + public static int $count = 100; + const TAG = "StrBox"; +} + +$intCls = "Box"; +$strCls = "Box"; + +// static::class returns the late-bound class. +var_dump($intCls::whoAmI()); +var_dump($strCls::whoAmI()); +var_dump(StrBox::whoAmI()); + +// static::$prop sees the late-bound class's storage. +$intCls::bump(); +$intCls::bump(); +$strCls::bump(); +StrBox::bump(); +var_dump($intCls::$count); +var_dump($strCls::$count); +var_dump(StrBox::$count); + +// static::FOO honours overrides via LSB. +var_dump($intCls::tag()); +var_dump(StrBox::tag()); + +// new static() returns the late-bound monomorph instance. +$a = $intCls::self_(); +$b = StrBox::self_(); +var_dump($a::class); +var_dump($b::class); +?> +--EXPECT-- +string(8) "Box" +string(11) "Box" +string(6) "StrBox" +int(2) +int(1) +int(101) +string(3) "Box" +string(6) "StrBox" +string(8) "Box" +string(6) "StrBox" diff --git a/Zend/tests/generics/runtime/monomorph_canonical_unions.phpt b/Zend/tests/generics/runtime/monomorph_canonical_unions.phpt new file mode 100644 index 000000000000..9eabeadfd3d3 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_canonical_unions.phpt @@ -0,0 +1,44 @@ +--TEST-- +Monomorph canonicalization: union and intersection arguments collapse to the same monomorph regardless of source order +--FILE-- + { + public function __construct(public mixed $value) {} +} + +// Union args canonicalize: order doesn't matter. +$a = new Box::(0); +$b = new Box::(0); +var_dump($a::class); +var_dump($b::class); +var_dump($a::class === $b::class); + +// Intersection args canonicalize: order doesn't matter. +$c = new Box::(new class implements Stringable1, Stringable2 { + public function s1(): string { return "s1"; } + public function s2(): string { return "s2"; } +}); +$d = new Box::(new class implements Stringable1, Stringable2 { + public function s1(): string { return "s1"; } + public function s2(): string { return "s2"; } +}); +var_dump($c::class); +var_dump($d::class); +var_dump($c::class === $d::class); + +// Two equivalent canonical forms produce the same monomorph class. +$x = new Box::(1); +$y = new Box::(1); +var_dump($x::class === $y::class); +?> +--EXPECT-- +string(15) "Box" +string(15) "Box" +bool(true) +string(28) "Box" +string(28) "Box" +bool(true) +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_class_exists.phpt b/Zend/tests/generics/runtime/monomorph_class_exists.phpt new file mode 100644 index 000000000000..99bdbf12c32a --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_class_exists.phpt @@ -0,0 +1,25 @@ +--TEST-- +Monomorph lookup: class_exists synthesizes the monomorph on demand +--FILE-- + { + public function __construct(public mixed $value) {} +} + +// Before any synthesis, class_exists on a canonical name should synthesize. +var_dump(class_exists("Box")); +var_dump(class_exists("Box")); +var_dump(class_exists("Box")); + +// Once synthesized, subsequent lookups hit the cached entry. +var_dump(class_exists("Box")); + +// Base class still exists. +var_dump(class_exists("Box")); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_dnf_canonicalization.phpt b/Zend/tests/generics/runtime/monomorph_dnf_canonicalization.phpt new file mode 100644 index 000000000000..3eda60d6ab58 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_dnf_canonicalization.phpt @@ -0,0 +1,50 @@ +--TEST-- +Monomorph canonicalization: DNF type-args sort to a canonical form (intersection-sorted, union-sorted) +--FILE-- + {} + +// Same canonical class entry regardless of source order. +$names = [ + new Box::<(A&B)|(C&D)>(), + new Box::<(B&A)|(D&C)>(), + new Box::<(C&D)|(A&B)>(), + new Box::<(A&B)|(D&C)>(), + new Box::<(B&A)|(C&D)>(), +]; + +$canonical = $names[0]::class; +echo "canonical: $canonical\n"; +foreach ($names as $i => $b) { + var_dump($b::class === $canonical); +} + +// Three-way union of intersections. +$x = new Box::<(A&B)|(C&D)|(D&E)>(); +$y = new Box::<(D&E)|(B&A)|(D&C)>(); +var_dump($x::class); +var_dump($y::class === $x::class); + +// Mixed scalars and intersections in a union. +$p = new Box::(); +$q = new Box::<(B&A)|string|int>(); +var_dump($p::class); +var_dump($q::class === $p::class); +?> +--EXPECT-- +canonical: Box<(A&B)|(C&D)> +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(22) "Box<(A&B)|(C&D)|(D&E)>" +bool(true) +string(21) "Box<(A&B)|int|string>" +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_dynamic_new.phpt b/Zend/tests/generics/runtime/monomorph_dynamic_new.phpt new file mode 100644 index 000000000000..ced054aa90a3 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_dynamic_new.phpt @@ -0,0 +1,34 @@ +--TEST-- +Monomorph lookup: `new $name()` with a canonical name string synthesizes on demand +--FILE-- + { + public function __construct(public mixed $value) {} +} + +// Pre-warm so the class entry exists for the explicit form too. +new Box::(0); + +$name = "Box"; +$obj = new $name(42); +var_dump($obj::class); +var_dump($obj instanceof Box); +var_dump($obj->value); + +// Names not yet synthesized are created on demand. +$other = "Box"; +$obj2 = new $other("hi"); +var_dump($obj2::class); +var_dump($obj2 instanceof Box); + +// Identity: two dynamic news of the same canonical name produce instances of the same class. +$obj3 = new $name(7); +var_dump($obj3::class === $obj::class); +?> +--EXPECT-- +string(8) "Box" +bool(true) +int(42) +string(11) "Box" +bool(true) +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_nested_args.phpt b/Zend/tests/generics/runtime/monomorph_nested_args.phpt new file mode 100644 index 000000000000..5510c3a59ddd --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_nested_args.phpt @@ -0,0 +1,37 @@ +--TEST-- +Monomorph nesting: type arguments may themselves be generic, canonicalized recursively +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class Pair { + public function __construct(public mixed $left, public mixed $right) {} +} + +// Nested generic args canonicalize. +$a = new Container::>(new Container::(1)); +var_dump($a::class); +var_dump($a instanceof Container); +var_dump($a->value::class); + +// Mixed nesting and unions. +$b = new Container::>(new Pair::(1, "x")); +var_dump($b::class); + +// Same nested form yields the same monomorph. +$c = new Container::>(new Container::(2)); +var_dump($a::class === $c::class); + +// Deeply nested. +$d = new Container::>>(new Container::>(new Container::(3))); +var_dump($d::class); +?> +--EXPECT-- +string(25) "Container>" +bool(true) +string(14) "Container" +string(27) "Container>" +bool(true) +string(36) "Container>>" diff --git a/Zend/tests/generics/runtime/monomorph_observables_sweep.phpt b/Zend/tests/generics/runtime/monomorph_observables_sweep.phpt new file mode 100644 index 000000000000..842bfaa817c0 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_observables_sweep.phpt @@ -0,0 +1,100 @@ +--TEST-- +Monomorph observables sweep: var_dump/export, exceptions, __toString, WeakMap/WeakRef, FCC, class_parents, class_implements +--FILE-- + { + public function __construct(public T $value) {} + public function __toString(): string { return "Box(" . $this->value . ")"; } +} + +class IntBoxChild extends Box {} + +$b = new Box::(42); + +// var_dump and var_export use the canonical mono name. +var_dump($b); +var_export($b); echo "\n"; + +// __toString shows the substituted T-typed property. +echo "toString: ", $b, "\n"; + +// TypeError when the substituted constructor signature is violated. The error +// message references the property's declaring class (the bare base), not the +// monomorph — that's the standard PHP behaviour for typed-property errors and +// matches where the slot's metadata lives. +try { + new Box::("not int"); +} catch (TypeError $e) { + echo "type-err raised: int property rejects string\n"; +} + +// get_class / ::class / get_parent_class. +var_dump(get_class($b)); +var_dump($b::class); +var_dump(get_parent_class("Box")); + +// class_parents / class_implements walk the mono chain. +$c = new IntBoxChild(1); +$parents = class_parents($c); +ksort($parents); +print_r(array_keys($parents)); +print_r(array_keys(class_implements($c))); + +// WeakMap keyed by a mono instance. +$wm = new WeakMap(); +$wm[$b] = "tag"; +var_dump($wm[$b]); + +// WeakReference also. +$wr = WeakReference::create($b); +var_dump($wr->get() === $b); + +// First-class callable on a mono method. +$f = $b->__toString(...); +var_dump($f()); + +// get_object_vars / get_class_vars / get_class_methods. +print_r(get_object_vars($b)); +print_r(get_class_vars("Box")); +$methods = get_class_methods("Box"); +sort($methods); +print_r($methods); +?> +--EXPECTF-- +object(Box)#%d (1) { + ["value"]=> + int(42) +} +\Box::__set_state(array( + 'value' => 42, +)) +toString: Box(42) +type-err raised: int property rejects string +string(8) "Box" +string(8) "Box" +string(3) "Box" +Array +( + [0] => Box + [1] => Box +) +Array +( + [0] => Stringable +) +string(3) "tag" +bool(true) +string(7) "Box(42)" +Array +( + [value] => 42 +) +Array +( + [value] => %w +) +Array +( + [0] => __construct + [1] => __toString +) diff --git a/Zend/tests/generics/runtime/monomorph_opcache_file_cache.phpt b/Zend/tests/generics/runtime/monomorph_opcache_file_cache.phpt new file mode 100644 index 000000000000..c727fe5032c1 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_opcache_file_cache.phpt @@ -0,0 +1,35 @@ +--TEST-- +Opcache file_cache: monomorphs synthesize freshly on each request; cached classes preserve canonical parent_name +--EXTENSIONS-- +opcache +--INI-- +opcache.enable_cli=1 +opcache.file_cache={TMP} +opcache.file_cache_only=1 +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class IntBox extends Box {} + +// Force the same set of synthesis events on every request. +$b = new IntBox(42); +echo "class: ", $b::class, "\n"; +echo "parent: ", (new ReflectionClass($b))->getParentClass()->getName(), "\n"; + +// Defaults monomorph also synthesizes. +$b2 = new Box(7); +echo "defaults: ", $b2::class, "\n"; + +// Dynamic name still hits the lookup hook on the second-load path. +$name = "Box"; +$b3 = new $name("hi"); +echo "dyn: ", $b3::class, "\n"; +?> +--EXPECT-- +class: IntBox +parent: Box +defaults: Box +dyn: Box diff --git a/Zend/tests/generics/runtime/monomorph_reflection.phpt b/Zend/tests/generics/runtime/monomorph_reflection.phpt new file mode 100644 index 000000000000..3605ef461108 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_reflection.phpt @@ -0,0 +1,32 @@ +--TEST-- +Monomorph reflection: ReflectionClass on a synthesized monomorph reports the canonical name and parent +--FILE-- + { + public function __construct(public mixed $value) {} + public function get(): mixed { return $this->value; } +} + +$b = new Box::(42); +$mono = $b::class; + +$rc = new ReflectionClass($mono); +var_dump($rc->getName()); + +$parent = $rc->getParentClass(); +var_dump($parent->getName()); + +// The monomorph carries the same method shape as the base. +var_dump($rc->hasMethod('get')); +var_dump($rc->getMethod('__construct')->getNumberOfParameters()); + +// Reflecting via the base name still works. +$rcBase = new ReflectionClass(Box::class); +var_dump($rcBase->getName()); +?> +--EXPECT-- +string(8) "Box" +string(3) "Box" +bool(true) +int(1) +string(3) "Box" diff --git a/Zend/tests/generics/runtime/monomorph_reflection_surface.phpt b/Zend/tests/generics/runtime/monomorph_reflection_surface.phpt new file mode 100644 index 000000000000..090d1b3ab0d6 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_reflection_surface.phpt @@ -0,0 +1,58 @@ +--TEST-- +Reflection surface for monomorphs: isGeneric, getGenericParameters, newInstance/Args/WithoutConstructor +--FILE-- + { + public function __construct(public mixed $value) {} + public function get(): T { return $this->value; } +} + +class NoDefault {} + +$rcBase = new ReflectionClass(Box::class); +$rcMono = new ReflectionClass("Box"); + +// Generic-template vs concrete-instantiation flags. +var_dump($rcBase->isGeneric()); +var_dump($rcMono->isGeneric()); +var_dump(count($rcBase->getGenericParameters())); +var_dump(count($rcMono->getGenericParameters())); + +// Reflecting the base class and calling newInstance synthesizes the defaults +// monomorph (same contract as `new Box()`). +$bb = $rcBase->newInstance(42); +var_dump($bb::class); + +// Reflecting a monomorph instantiates that monomorph. +$bm = $rcMono->newInstance(7); +var_dump($bm::class); + +// newInstanceArgs honours the same synthesis path. +$ba = $rcMono->newInstanceArgs([99]); +var_dump($ba::class); + +// newInstanceWithoutConstructor too. +$bw = (new ReflectionClass("Box"))->newInstanceWithoutConstructor(); +var_dump($bw::class); + +// And newInstance on a no-defaults generic throws. +try { + (new ReflectionClass(NoDefault::class))->newInstance(); +} catch (Error $e) { + echo "err: ", $e->getMessage(), "\n"; +} + +// Substituted method signature is visible on the monomorph. +echo "Box::get returns: ", $rcMono->getMethod('get')->getReturnType(), "\n"; +?> +--EXPECT-- +bool(true) +bool(false) +int(1) +int(0) +string(8) "Box" +string(8) "Box" +string(8) "Box" +string(11) "Box" +err: Cannot instantiate generic class NoDefault without type arguments; type parameter T has no default +Box::get returns: int diff --git a/Zend/tests/generics/runtime/monomorph_serialize.phpt b/Zend/tests/generics/runtime/monomorph_serialize.phpt new file mode 100644 index 000000000000..f6509147df63 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_serialize.phpt @@ -0,0 +1,29 @@ +--TEST-- +Monomorph serialization: serialize/unserialize round-trips through the canonical class name +--FILE-- + { + public function __construct(public mixed $value) {} +} + +$b = new Box::(42); +$s = serialize($b); + +// The serialized payload encodes the canonical class name. +echo $s, "\n"; + +// Unserialize materializes the monomorph (synthesizing if not yet present). +$b2 = unserialize($s); +var_dump($b2::class); +var_dump($b2 instanceof Box); +var_dump($b2->value); + +// Round-trip identity through the canonical name. +var_dump($b::class === $b2::class); +?> +--EXPECTF-- +O:8:"Box":1:{s:5:"value";i:42;} +string(8) "Box" +bool(true) +int(42) +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_typed_class_constants.phpt b/Zend/tests/generics/runtime/monomorph_typed_class_constants.phpt new file mode 100644 index 000000000000..384f5579513f --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_typed_class_constants.phpt @@ -0,0 +1,36 @@ +--TEST-- +Typed class constants: per-monomorph substituted type info +--FILE-- + { + const T DEFAULT_VALUE = 0; + const string TAG = "Box"; // non-generic, shared as-is +} + +$rcBase = new ReflectionClass(Box::class); +echo "Box::DEFAULT_VALUE type=", $rcBase->getReflectionConstant("DEFAULT_VALUE")->getType(), "\n"; +echo "Box::TAG type=", $rcBase->getReflectionConstant("TAG")->getType(), "\n"; + +$rcInt = new ReflectionClass("Box"); +echo "Box::DEFAULT_VALUE type=", $rcInt->getReflectionConstant("DEFAULT_VALUE")->getType(), "\n"; +echo "Box::TAG type=", $rcInt->getReflectionConstant("TAG")->getType(), "\n"; + +$rcStr = new ReflectionClass("Box"); +echo "Box::DEFAULT_VALUE type=", $rcStr->getReflectionConstant("DEFAULT_VALUE")->getType(), "\n"; + +$rcUnion = new ReflectionClass("Box"); +echo "Box::DEFAULT_VALUE type=", $rcUnion->getReflectionConstant("DEFAULT_VALUE")->getType(), "\n"; + +// Constant values stay shared (immutable) — substitution only changes the type. +var_dump(Box::DEFAULT_VALUE); +var_dump((new ReflectionClass("Box"))->getReflectionConstant("DEFAULT_VALUE")->getValue()); +?> +--EXPECT-- +Box::DEFAULT_VALUE type=mixed +Box::TAG type=string +Box::DEFAULT_VALUE type=int +Box::TAG type=string +Box::DEFAULT_VALUE type=?string +Box::DEFAULT_VALUE type=int|float +int(0) +int(0) diff --git a/Zend/tests/generics/runtime/naked_new_self_no_defaults_lenient.phpt b/Zend/tests/generics/runtime/naked_new_self_no_defaults_lenient.phpt new file mode 100644 index 000000000000..45be1e78404d --- /dev/null +++ b/Zend/tests/generics/runtime/naked_new_self_no_defaults_lenient.phpt @@ -0,0 +1,27 @@ +--TEST-- +Lexical `new self()` and `new ThisClass()` in a no-defaults generic class fall back to a bare instance +--FILE-- + with no default doesn't error — it creates a +// bare Box instance, preserving the lexical-self semantic where T is in scope +// but can't be statically bound to a concrete type at the call site. +final readonly class Box<+T> { + public function __construct(public T $value) {} + public function cloneSelf(): self { return new self($this->value); } + public function cloneByName(): Box { return new Box($this->value); } +} + +$b = new Box::(42); +$c1 = $b->cloneSelf(); +$c2 = $b->cloneByName(); + +var_dump($c1::class); +var_dump($c2::class); +var_dump($c1->value); +var_dump($c2->value); +?> +--EXPECT-- +string(3) "Box" +string(3) "Box" +int(42) +int(42) diff --git a/Zend/tests/generics/runtime/naked_new_self_static_parent.phpt b/Zend/tests/generics/runtime/naked_new_self_static_parent.phpt new file mode 100644 index 000000000000..c0bc09c4c1bf --- /dev/null +++ b/Zend/tests/generics/runtime/naked_new_self_static_parent.phpt @@ -0,0 +1,40 @@ +--TEST-- +Naked new self/static/parent inside generic class bodies +--FILE-- + { + public function __construct(public mixed $value) {} + public static function makeSelf(mixed $v): self { return new self($v); } + public static function makeStatic(mixed $v): static { return new static($v); } + public static function makeBare(mixed $v): Box { return new Box($v); } +} + +class IntBox extends Box { + public static function makeStatic(mixed $v): static { return new static($v); } + public function makeParent(): Box { return new parent($this->value); } +} + +class StrBox extends Box { + public function makeParent(): Box { return new parent($this->value); } +} + +// `new self()` → lexical class with defaults applied. +var_dump(Box::makeSelf(1)::class); +// `new Box()` (literal) inside Box's static method also routes through the +// same rewrite (lexical self-reference). +var_dump(Box::makeBare(2)::class); +// `new static()` from Box → defaults mono; from IntBox → IntBox (no params). +var_dump(Box::makeStatic(3)::class); +var_dump(IntBox::makeStatic(4)::class); + +// `new parent()` uses the extends args. +var_dump((new IntBox(5))->makeParent()::class); +var_dump((new StrBox("x"))->makeParent()::class); +?> +--EXPECT-- +string(8) "Box" +string(8) "Box" +string(8) "Box" +string(6) "IntBox" +string(8) "Box" +string(11) "Box" diff --git a/Zend/tests/generics/runtime/static_property_isolation.phpt b/Zend/tests/generics/runtime/static_property_isolation.phpt new file mode 100644 index 000000000000..8c40c14b3a25 --- /dev/null +++ b/Zend/tests/generics/runtime/static_property_isolation.phpt @@ -0,0 +1,47 @@ +--TEST-- +Monomorph isolation: each synthesized monomorph has its own static-property storage +--FILE-- + { + public static int $count = 0; + public static array $items = []; +} + +// Force synthesis of two distinct monomorphs. +new Counter::(); +new Counter::(); + +$intCls = "Counter"; +$strCls = "Counter"; + +$intCls::$count = 5; +$strCls::$count = 10; + +$intCls::$items[] = "int-a"; +$intCls::$items[] = "int-b"; +$strCls::$items[] = "string-only"; + +var_dump($intCls::$count); +var_dump($strCls::$count); +var_dump(Counter::$count); + +var_dump($intCls::$items); +var_dump($strCls::$items); +var_dump(Counter::$items); +?> +--EXPECT-- +int(5) +int(10) +int(0) +array(2) { + [0]=> + string(5) "int-a" + [1]=> + string(5) "int-b" +} +array(1) { + [0]=> + string(11) "string-only" +} +array(0) { +} diff --git a/Zend/tests/generics/syntax/turbofish_new.phpt b/Zend/tests/generics/syntax/turbofish_new.phpt index bc71fbbe37f0..c79c15f90a4b 100644 --- a/Zend/tests/generics/syntax/turbofish_new.phpt +++ b/Zend/tests/generics/syntax/turbofish_new.phpt @@ -29,4 +29,4 @@ echo get_class($c), "\n"; 1 2 3 -Container +Container diff --git a/Zend/tests/generics/traits/abstract_method_violates.phpt b/Zend/tests/generics/traits/abstract_method_violates.phpt index 9b7179dce1fd..d1abe4e84cb0 100644 --- a/Zend/tests/generics/traits/abstract_method_violates.phpt +++ b/Zend/tests/generics/traits/abstract_method_violates.phpt @@ -12,4 +12,4 @@ class A { } ?> --EXPECTF-- -Fatal error: Declaration of A::foo(int $v): int must be compatible with Thing::foo(string $v): string in %s on line %d +Fatal error: Declaration of A::foo(int $v): int must be compatible with Thing::foo(string $v): string in %s on line %d diff --git a/Zend/tests/generics/turbofish/arity/new_arity.phpt b/Zend/tests/generics/turbofish/arity/new_arity.phpt index de36227a7502..4f69cf3663c4 100644 --- a/Zend/tests/generics/turbofish/arity/new_arity.phpt +++ b/Zend/tests/generics/turbofish/arity/new_arity.phpt @@ -14,7 +14,9 @@ catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } try { new Plain::(); } catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } -// Pair: K required, V has default - so 1 or 2 args is OK, 0 or 3 is not +// Pair: K required, V has default - so 1 or 2 args is OK, 0 or 3 is not. +// With monomorphization, the missing trailing arg is filled from the default, +// so the canonical name reflects the substituted Pair. $p = new Pair::(); echo get_class($p), "\n"; @@ -24,5 +26,5 @@ catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } --EXPECT-- Too many generic type arguments to new Box, 2 passed and exactly 1 expected Too many generic type arguments to new Plain, 1 passed and exactly 0 expected -Pair +Pair Too many generic type arguments to new Pair, 3 passed and at most 2 expected diff --git a/Zend/tests/generics/turbofish/naked_new_with_defaults.phpt b/Zend/tests/generics/turbofish/naked_new_with_defaults.phpt new file mode 100644 index 000000000000..55afc9bc1f54 --- /dev/null +++ b/Zend/tests/generics/turbofish/naked_new_with_defaults.phpt @@ -0,0 +1,29 @@ +--TEST-- +Naked `new GenericClass()` synthesizes the monomorph when every parameter has a default +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class Pair { + public function __construct(public mixed $left, public mixed $right) {} +} + +$b = new Box(42); +var_dump($b::class); +var_dump($b instanceof Box); + +$p = new Pair(1, "hi"); +var_dump($p::class); +var_dump($p instanceof Pair); + +// Identity: two naked-new's of the same class produce instances of the same monomorph. +var_dump((new Box(1))::class === (new Box(2))::class); +?> +--EXPECT-- +string(8) "Box" +bool(true) +string(16) "Pair" +bool(true) +bool(true) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 4d78f783726d..3c7f1d7404b0 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1050,7 +1050,7 @@ static zend_generic_type_table *zend_generic_get_or_create_op_array_table(zend_o } /* Ensure ce->generic_types is allocated, then return it. */ -static zend_generic_type_table *zend_generic_get_or_create_class_table(zend_class_entry *ce) +ZEND_API zend_generic_type_table *zend_generic_get_or_create_class_table(zend_class_entry *ce) { if (!ce->generic_types) { ce->generic_types = zend_generic_type_table_alloc(); @@ -2216,6 +2216,219 @@ ZEND_API zend_string *zend_type_to_string(zend_type type) { return zend_type_to_string_resolved(type, NULL); } +/* === Monomorph canonicalization === + * + * Produces a deterministic class-name string for a generic application. + * Properties: + * - No whitespace anywhere. + * - Class names appear in fully-resolved form (the caller is expected to + * pass already-resolved zend_types; type-param refs are not allowed). + * - Members of union and intersection lists are sorted by canonical + * sub-string, so int|string and string|int collapse to "int|string". + * - `?T` is normalized to the union form `T|null`. + * - Intersection branches inside a union are wrapped in parens: "(A&B)|C". + * - Nested generic applications recurse: Box>. + * + * Two type-arg lists that PHP would treat as equivalent types must produce + * the same canonical string. */ + +static int zend_canonical_strptr_cmp(const void *a, const void *b) +{ + const zend_string *const *sa = a; + const zend_string *const *sb = b; + return zend_binary_strcmp( + ZSTR_VAL(*sa), ZSTR_LEN(*sa), + ZSTR_VAL(*sb), ZSTR_LEN(*sb)); +} + +static zend_string *zend_canonical_join_sorted( + zend_string **pieces, uint32_t count, char sep) +{ + qsort(pieces, count, sizeof(zend_string *), zend_canonical_strptr_cmp); + smart_str buf = {0}; + for (uint32_t i = 0; i < count; i++) { + if (i > 0) smart_str_appendc(&buf, sep); + smart_str_append(&buf, pieces[i]); + } + smart_str_0(&buf); + return buf.s; +} + +static void zend_canonical_append_scalar_pieces( + uint32_t type_mask, zend_string ***pieces, uint32_t *count, uint32_t *cap) +{ + /* Fixed-order scalar emission so each scalar bit yields exactly one + * piece; the outer sort then merges these with class-name pieces. */ + struct { uint32_t bit; zend_string *str; } table[] = { + { MAY_BE_CALLABLE, ZSTR_KNOWN(ZEND_STR_CALLABLE) }, + { MAY_BE_OBJECT, ZSTR_KNOWN(ZEND_STR_OBJECT) }, + { MAY_BE_ARRAY, ZSTR_KNOWN(ZEND_STR_ARRAY) }, + { MAY_BE_STRING, ZSTR_KNOWN(ZEND_STR_STRING) }, + { MAY_BE_LONG, ZSTR_KNOWN(ZEND_STR_INT) }, + { MAY_BE_DOUBLE, ZSTR_KNOWN(ZEND_STR_FLOAT) }, + { MAY_BE_VOID, ZSTR_KNOWN(ZEND_STR_VOID) }, + { MAY_BE_NEVER, ZSTR_KNOWN(ZEND_STR_NEVER) }, + }; + for (size_t i = 0; i < sizeof(table)/sizeof(table[0]); i++) { + if (type_mask & table[i].bit) { + if (*count == *cap) { + *cap = *cap ? *cap * 2 : 4; + *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); + } + (*pieces)[(*count)++] = zend_string_copy(table[i].str); + } + } + if ((type_mask & MAY_BE_BOOL) == MAY_BE_BOOL) { + if (*count == *cap) { *cap = *cap ? *cap * 2 : 4; *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); } + (*pieces)[(*count)++] = zend_string_copy(ZSTR_KNOWN(ZEND_STR_BOOL)); + } else { + if (type_mask & MAY_BE_FALSE) { + if (*count == *cap) { *cap = *cap ? *cap * 2 : 4; *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); } + (*pieces)[(*count)++] = zend_string_copy(ZSTR_KNOWN(ZEND_STR_FALSE)); + } + if (type_mask & MAY_BE_TRUE) { + if (*count == *cap) { *cap = *cap ? *cap * 2 : 4; *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); } + (*pieces)[(*count)++] = zend_string_copy(ZSTR_KNOWN(ZEND_STR_TRUE)); + } + } + if (type_mask & MAY_BE_NULL) { + if (*count == *cap) { *cap = *cap ? *cap * 2 : 4; *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); } + (*pieces)[(*count)++] = zend_string_copy(ZSTR_KNOWN(ZEND_STR_NULL_LOWERCASE)); + } +} + +static zend_string *zend_canonical_one(zend_type type); + +static zend_string *zend_canonical_intersection(const zend_type_list *list) +{ + zend_string **pieces = safe_emalloc(list->num_types, sizeof(zend_string *), 0); + uint32_t n = 0; + const zend_type *single; + ZEND_TYPE_LIST_FOREACH(list, single) { + /* Intersection branches are class-name leaves or NAMED_WITH_ARGS. */ + pieces[n++] = zend_canonical_one(*single); + } ZEND_TYPE_LIST_FOREACH_END(); + zend_string *result = zend_canonical_join_sorted(pieces, n, '&'); + for (uint32_t i = 0; i < n; i++) zend_string_release(pieces[i]); + efree(pieces); + return result; +} + +static zend_string *zend_canonical_one(zend_type type) +{ + uint32_t scalar_mask = ZEND_TYPE_PURE_MASK(type); + /* MAY_BE_ANY appearing alone is "mixed"; otherwise it's an unbounded + * bag of scalar bits we expand individually. */ + bool is_mixed_pure = scalar_mask == MAY_BE_ANY + && !ZEND_TYPE_HAS_LIST(type) + && !ZEND_TYPE_HAS_NAME(type) + && !ZEND_TYPE_HAS_NAMED_WITH_ARGS(type); + if (is_mixed_pure) { + return zend_string_copy(ZSTR_KNOWN(ZEND_STR_MIXED)); + } + + zend_string **pieces = NULL; + uint32_t n = 0, cap = 0; + + if (ZEND_TYPE_IS_INTERSECTION(type)) { + zend_string *s = zend_canonical_intersection(ZEND_TYPE_LIST(type)); + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = s; + } else if (ZEND_TYPE_HAS_LIST(type)) { + const zend_type *list_type; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), list_type) { + zend_string *s; + if (ZEND_TYPE_IS_INTERSECTION(*list_type)) { + zend_string *inner = zend_canonical_intersection(ZEND_TYPE_LIST(*list_type)); + s = zend_string_concat3("(", 1, ZSTR_VAL(inner), ZSTR_LEN(inner), ")", 1); + zend_string_release(inner); + } else { + s = zend_canonical_one(*list_type); + } + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = s; + } ZEND_TYPE_LIST_FOREACH_END(); + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); + smart_str buf = {0}; + if (nwa->name) smart_str_append(&buf, nwa->name); + smart_str_appendc(&buf, '<'); + for (uint32_t i = 0; i < nwa->count; i++) { + if (i > 0) smart_str_appendc(&buf, ','); + zend_string *inner = zend_canonical_one(nwa->args[i]); + smart_str_append(&buf, inner); + zend_string_release(inner); + } + smart_str_appendc(&buf, '>'); + smart_str_0(&buf); + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = buf.s; + } else if (ZEND_TYPE_HAS_NAME(type)) { + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = zend_string_copy(ZEND_TYPE_NAME(type)); + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + /* Canonicalization runs against already-substituted types; a stray + * T-ref means somebody handed us pre-erasure data by mistake. */ + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = ref->name ? zend_string_copy(ref->name) : ZSTR_EMPTY_ALLOC(); + } + + zend_canonical_append_scalar_pieces(scalar_mask, &pieces, &n, &cap); + + zend_string *result; + if (n == 0) { + result = ZSTR_EMPTY_ALLOC(); + } else if (n == 1) { + result = zend_string_copy(pieces[0]); + } else { + result = zend_canonical_join_sorted(pieces, n, '|'); + } + for (uint32_t i = 0; i < n; i++) zend_string_release(pieces[i]); + if (pieces) efree(pieces); + return result; +} + +ZEND_API zend_string *zend_type_to_canonical_string(zend_type type) +{ + return zend_canonical_one(type); +} + +ZEND_API bool zend_type_contains_type_parameter(zend_type type) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) return true; + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_type_parameter(nwa->args[i])) return true; + } + } + if (ZEND_TYPE_HAS_LIST(type)) { + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), member) { + if (zend_type_contains_type_parameter(*member)) return true; + } ZEND_TYPE_LIST_FOREACH_END(); + } + return false; +} + +ZEND_API zend_string *zend_generic_canonical_class_name( + zend_string *base_name, const zend_type *args, uint32_t arity) +{ + smart_str buf = {0}; + smart_str_append(&buf, base_name); + smart_str_appendc(&buf, '<'); + for (uint32_t i = 0; i < arity; i++) { + if (i > 0) smart_str_appendc(&buf, ','); + zend_string *piece = zend_canonical_one(args[i]); + smart_str_append(&buf, piece); + zend_string_release(piece); + } + smart_str_appendc(&buf, '>'); + smart_str_0(&buf); + return buf.s; +} + static bool is_generator_compatible_class_type(const zend_string *name) { return zend_string_equals_ci(name, ZSTR_KNOWN(ZEND_STR_TRAVERSABLE)) || zend_string_equals_literal_ci(name, "Iterator") @@ -6415,6 +6628,10 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ } zend_class_entry *ce = NULL; + /* If parent_extends_args is non-NULL, we resolved `new parent()` where the + * active class's extends clause specified type arguments — in that case we + * use the user-supplied args directly rather than the parent's defaults. */ + const zend_type *parent_extends_args = NULL; if (opline->op1_type == IS_CONST) { zend_string *lcname = Z_STR_P(CT_CONSTANT(opline->op1) + 1); ce = zend_hash_find_ptr(CG(class_table), lcname); @@ -6427,12 +6644,92 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ ce = CG(active_class_entry); } } else if (opline->op1_type == IS_UNUSED - && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF && zend_is_scope_known()) { - ce = CG(active_class_entry); + uint32_t fetch = opline->op1.num & ZEND_FETCH_CLASS_MASK; + if (fetch == ZEND_FETCH_CLASS_SELF) { + ce = CG(active_class_entry); + } else if (fetch == ZEND_FETCH_CLASS_PARENT + && CG(active_class_entry) + && CG(active_class_entry)->parent_name) { + zend_string *lc_parent = zend_string_tolower(CG(active_class_entry)->parent_name); + ce = zend_hash_find_ptr(CG(class_table), lc_parent); + zend_string_release(lc_parent); + /* `new parent()` honours the active class's `extends Box` args + * if present, so it instantiates the same monomorph the active + * class extends rather than the parent's defaults. */ + if (ce && CG(active_class_entry)->generic_types + && CG(active_class_entry)->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS( + *CG(active_class_entry)->generic_types->extends) + && !zend_type_contains_type_parameter( + *CG(active_class_entry)->generic_types->extends)) { + parent_extends_args = CG(active_class_entry)->generic_types->extends; + } + } + } + + /* Naked `new GenericClass()` (no turbofish): build the canonical monomorph + * name and rewrite op1 to it so the lookup hook synthesizes the monomorph. + * + * Handles three resolved-at-compile-time forms: + * - literal class name → use its declared defaults; error if any param + * has no default (the user has no way to bind T here) + * - `self` → use the active class's declared defaults; if any param has + * no default, leave the opcode alone and the runtime creates a bare + * instance (preserving the existing lexical-self semantic and letting + * code inside a generic class refer to itself without forcing a + * defaults declaration) + * - `parent` → use the args from the `extends Foo<...>` clause if any, + * otherwise the parent's defaults; same lenient fallback as `self` + * + * `static` and dynamic names are handled at runtime in ZEND_NEW. */ + if (ce && ce->generic_parameters && !turbofish_ast) { + uint32_t count = ce->generic_parameters->count; + const zend_type *src_args = NULL; + uint32_t src_arity = 0; + if (parent_extends_args) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*parent_extends_args); + src_args = nwa->args; + src_arity = nwa->count; + } + zend_type chosen[ZEND_GENERIC_MAX_PARAMS]; + bool ok = true; + /* Strict (compile-error on missing defaults) when the call site refers + * to a generic class from outside that class. Lenient (fall through to + * the bare class) when it's a lexical self-reference — including both + * `new self()` and `new ThisClass()` inside the class's own body — + * since the lexical scope semantically has access to T even though we + * can't bind it at compile time. */ + bool is_lexical_self = ce == CG(active_class_entry); + for (uint32_t i = 0; i < count; i++) { + if (i < src_arity) { + chosen[i] = src_args[i]; + continue; + } + const zend_generic_parameter *p = &ce->generic_parameters->parameters[i]; + if (!ZEND_TYPE_IS_SET(p->default_type)) { + if (!is_lexical_self) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot instantiate generic class %s without type arguments; " + "type parameter %s has no default", + ZSTR_VAL(ce->name), ZSTR_VAL(p->name)); + } + ok = false; + break; + } + chosen[i] = ZEND_TYPE_IS_SET(p->default_pre_erasure) + ? p->default_pre_erasure : p->default_type; + } + if (ok) { + zend_string *canonical = + zend_generic_canonical_class_name(ce->name, chosen, count); + opline->op1_type = IS_CONST; + opline->op1.constant = zend_add_class_name_literal(canonical); + opline->op2.num = zend_alloc_cache_slot(); + } } - const zend_function *fbc = NULL; if (ce && ce->default_object_handlers->get_constructor == zend_std_get_constructor @@ -10226,6 +10523,15 @@ static void zend_compile_class_const_decl(zend_ast *ast, uint32_t flags, zend_as zend_error_noreturn(E_COMPILE_ERROR, "Class constant %s::%s cannot have type %s", ZSTR_VAL(ce->name), ZSTR_VAL(name), ZSTR_VAL(type_str)); } + + /* Capture the pre-erasure type when the declared type references a + * generic type parameter, so monomorph synthesis can substitute + * T → arg in the mono's class-constant type. */ + if (zend_type_ast_has_generic_content(type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(type_ast); + zend_generic_type_table_set_class_constant( + zend_generic_get_or_create_class_table(ce), name, pre); + } } if (UNEXPECTED((flags & ZEND_ACC_PRIVATE) && (flags & ZEND_ACC_FINAL))) { @@ -10552,15 +10858,27 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top ZEND_ASSERT(!(decl->flags & ZEND_ACC_ANON_CLASS)); ce->generic_parameters = zend_compile_generic_type_parameter_list(decl->child[5], ZEND_GENERIC_ORIGIN_CLASS_LIKE); zend_generic_scope_push(ce->generic_parameters, ZEND_GENERIC_ORIGIN_CLASS_LIKE); + bool all_defaults = true; + for (uint32_t i = 0; i < ce->generic_parameters->count; i++) { + if (!ZEND_TYPE_IS_SET(ce->generic_parameters->parameters[i].default_type)) { + all_defaults = false; + break; + } + } + if (all_defaults) { + ce->ce_flags |= ZEND_ACC_GENERIC_ALL_DEFAULTS; + } } if (extends_ast) { ce->parent_name = zend_resolve_const_class_name_reference(extends_ast, "class name"); /* Capture the pre-erasure type when the extends clause specifies type - * arguments (e.g. `extends Box`). For an interface declaration - * this AST holds the list of parent interfaces; for class/trait it's - * the single parent class. */ + * arguments (e.g. `extends Box`). At link time + * (`zend_do_link_class`) this side-table entry triggers + * pre-synthesis of the `Box` monomorph and a rewrite of + * `parent_name` to the canonical name, so this class's direct + * parent is the monomorph and `instanceof Box` holds. */ if (extends_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { zend_type pre = zend_compile_pre_erasure_typename(extends_ast); zend_generic_type_table_set_extends( diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 2b9302d8e57f..6be418cbf0d4 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -182,6 +182,7 @@ ZEND_API zend_generic_type_table *zend_generic_type_table_alloc(void); ZEND_API void zend_generic_type_table_destroy(zend_generic_type_table *table); ZEND_API void zend_generic_type_table_set_return(zend_generic_type_table *t, zend_type type); ZEND_API void zend_generic_type_table_set_extends(zend_generic_type_table *t, zend_type type); +ZEND_API zend_generic_type_table *zend_generic_get_or_create_class_table(zend_class_entry *ce); ZEND_API void zend_generic_type_table_set_parameter(zend_generic_type_table *t, uint32_t idx, zend_type type); ZEND_API void zend_generic_type_table_set_property(zend_generic_type_table *t, zend_string *name, zend_type type); ZEND_API void zend_generic_type_table_set_class_constant(zend_generic_type_table *t, zend_string *name, zend_type type); @@ -418,6 +419,11 @@ typedef struct _zend_oparray_context { /* Class cannot be serialized or unserialized | | | */ #define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */ /* | | | */ +/* Generic class whose every parameter has a default — | | | */ +/* hot-path bit for ZEND_NEW to skip the per-parameter | | | */ +/* default scan when synthesizing a defaults monomorph. | | | */ +#define ZEND_ACC_GENERIC_ALL_DEFAULTS (1u << 31) /* X | | | */ +/* | | | */ /* Class Flags 2 (ce_flags2) (unused: 0-31) | | | */ /* ========================= | | | */ /* | | | */ @@ -1120,6 +1126,15 @@ void zend_assert_valid_class_name(const zend_string *const_name, const char *typ zend_string *zend_type_to_string_resolved(zend_type type, const zend_class_entry *scope); ZEND_API zend_string *zend_type_to_string(zend_type type); +/* Monomorphization: a canonical form of a generic class application like + * "Box>". Stable across permutations of union/intersection + * order so that semantically equivalent type-arg lists hash to the same name + * and resolve to the same synthesized class entry. */ +ZEND_API zend_string *zend_type_to_canonical_string(zend_type type); +ZEND_API zend_string *zend_generic_canonical_class_name( + zend_string *base_name, const zend_type *args, uint32_t arity); +ZEND_API bool zend_type_contains_type_parameter(zend_type type); + /* class fetches */ #define ZEND_FETCH_CLASS_DEFAULT 0 #define ZEND_FETCH_CLASS_SELF 1 diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 71e0c56a51c8..e99e4e3e7e8e 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1235,6 +1235,26 @@ ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, zend_string * return ce; } + /* Monomorphization hook: if the requested name has generic shape + * ("Box"), parse it and synthesize the monomorph. This makes + * unserialize, dynamic `new $name`, `class_exists`, and `instanceof + * "Foo"` all transparently materialize the monomorph on demand. + * Runs before autoload so a Box name doesn't trigger a doomed + * autoload for "Box" itself. The synthesizer only succeeds when + * the base class exists and is generic. */ + if (!zend_is_compiling() && zend_class_name_is_monomorph(name)) { + zend_class_entry *mono = zend_try_synthesize_monomorph_by_name(name, flags); + if (mono) { + if (!key) zend_string_release_ex(lc_name, 0); + if (ce_cache) SET_CE_CACHE(ce_cache, mono); + return mono; + } + if (EG(exception)) { + if (!key) zend_string_release_ex(lc_name, 0); + return NULL; + } + } + /* The compiler is not-reentrant. Make sure we autoload only during run-time. */ if ((flags & ZEND_FETCH_CLASS_NO_AUTOLOAD) || zend_is_compiling()) { if (!key) { diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 35e6d15df661..11838047007f 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2754,11 +2754,18 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par arity = ZEND_TYPE_NAMED_WITH_ARGS(*extends_args)->count; } - if (arity > 0 || parent_ce->generic_parameters) { - zend_check_generic_link_arity(parent_ce, arity, "extends", ce->name); - } + /* If parent_ce is a synthesized monomorph (its name carries `<`), + * the type args were already validated against the base's bounds + * during synthesis. The side-table args we have here refer to the + * original base, not the mono — re-checking them against the + * mono's (empty) generic_parameters would spuriously fail. */ + if (!zend_class_is_monomorph(parent_ce)) { + if (arity > 0 || parent_ce->generic_parameters) { + zend_check_generic_link_arity(parent_ce, arity, "extends", ce->name); + } - zend_check_generic_link_bounds(parent_ce, extends_args, "extends", ce); + zend_check_generic_link_bounds(parent_ce, extends_args, "extends", ce); + } } if (UNEXPECTED(ce->ce_flags & ZEND_ACC_INTERFACE)) { @@ -3421,6 +3428,16 @@ static void zend_substitute_trait_method_arg_info( uint32_t return_slot_offset = has_return ? 1 : 0; + /* The substituted `sub` zend_type comes from compiling the pre-erasure + * AST and carries only the type bits — none of the arg_info-level flags + * (send mode / by-ref, variadic, promoted, tentative) which live in the + * type_mask's extra-flag region. Preserve those from the original slot. */ + const uint32_t arg_extra_flags_mask = + (3u << _ZEND_SEND_MODE_SHIFT) + | _ZEND_IS_VARIADIC_BIT + | _ZEND_IS_PROMOTED_BIT + | _ZEND_IS_TENTATIVE_BIT; + if (has_return && orig_op->generic_types->return_type) { const zend_type *pre = orig_op->generic_types->return_type; if (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre)) { @@ -3429,7 +3446,9 @@ static void zend_substitute_trait_method_arg_info( if (!new_block) { new_block = zend_clone_arg_info_block(orig_block, total); } + uint32_t carry = ZEND_TYPE_FULL_MASK(new_block[0].type) & arg_extra_flags_mask; new_block[0].type = sub; + ZEND_TYPE_FULL_MASK(new_block[0].type) |= carry; zend_type_copy_ctor(&new_block[0].type, /* use_arena */ true, /* persistent */ false); } } @@ -3456,7 +3475,9 @@ static void zend_substitute_trait_method_arg_info( if (!new_block) { new_block = zend_clone_arg_info_block(orig_block, total); } + uint32_t carry = ZEND_TYPE_FULL_MASK(new_block[return_slot_offset + idx].type) & arg_extra_flags_mask; new_block[return_slot_offset + idx].type = sub; + ZEND_TYPE_FULL_MASK(new_block[return_slot_offset + idx].type) |= carry; zend_type_copy_ctor(&new_block[return_slot_offset + idx].type, /* use_arena */ true, /* persistent */ false); } ZEND_HASH_FOREACH_END(); } @@ -5506,6 +5527,12 @@ static void zend_validate_generic_inheritance_arities( for (uint32_t i = 0; i < ce->num_traits; i++) { zend_class_entry *trait_ce = traits_and_interfaces[i]; if (!trait_ce) continue; + /* Skip when the resolved trait is a synthesized monomorph — its args + * were already validated against the base trait's bounds at synthesis + * time and the side-table args are pre-rewrite (refer to the base). */ + if (zend_class_is_monomorph(trait_ce)) { + continue; + } uint32_t arity = zend_lookup_inheritance_arity(trait_uses_table, i); if (arity > 0 || trait_ce->generic_parameters) { zend_check_generic_link_arity(trait_ce, arity, "use", ce->name); @@ -5519,6 +5546,10 @@ static void zend_validate_generic_inheritance_arities( for (uint32_t i = 0; i < ce->num_interfaces; i++) { zend_class_entry *iface_ce = traits_and_interfaces[ce->num_traits + i]; if (!iface_ce) continue; + /* Same monomorph-skip as the trait loop above. */ + if (zend_class_is_monomorph(iface_ce)) { + continue; + } uint32_t arity = zend_lookup_inheritance_arity(implements_table, i); if (arity > 0 || iface_ce->generic_parameters) { zend_check_generic_link_arity(iface_ce, arity, clause, ce->name); @@ -5528,6 +5559,30 @@ static void zend_validate_generic_inheritance_arities( } } +/* Mark entries in `names` that share an lc_name with another entry — the + * diamond-of-same-base case for `implements Sink, Sink` and + * `use Box, Box`. The marked indices fall back to the bare-base + * + side-table inheritance path so the existing diamond-merge machinery in + * `do_inherit_method` can compute the merged signature. Skipped entirely when + * count <= 1 (the common case). */ +static void zend_mark_duplicate_lc_names( + const zend_class_name *names, uint32_t count, bool *out) +{ + if (count <= 1) { + memset(out, 0, sizeof(bool) * count); + return; + } + memset(out, 0, sizeof(bool) * count); + for (uint32_t a = 0; a < count; a++) { + for (uint32_t b = a + 1; b < count; b++) { + if (zend_string_equals(names[a].lc_name, names[b].lc_name)) { + out[a] = true; + out[b] = true; + } + } + } +} + ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name, const zend_string *key) /* {{{ */ { /* Load parent/interface dependencies first, so we can still gracefully abort linking @@ -5540,41 +5595,167 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string uint32_t is_cacheable = ce->ce_flags & ZEND_ACC_IMMUTABLE; uint32_t i, j; zval *zv; + zend_string *synthesized_lc_parent = NULL; ALLOCA_FLAG(use_heap) SET_ALLOCA_FLAG(use_heap); ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_LINKED)); + /* Bound-erased generics, extends-with-args: when this class declared + * `extends Box`, its compile-time side table holds the pre-erasure + * `Box` payload while `parent_name` is the bare base name. Before + * touching anything else, synthesize the `Box` monomorph as a + * fully separate top-level link and rewrite `parent_name` to the + * canonical name. That makes this class's direct parent be the + * monomorph entry, so `$this instanceof Box` holds and inherited + * substituted member types come through the normal inheritance + * pipeline. Doing it here (rather than inline during compile) keeps + * the synthesizer's recursive link sequential — it completes before + * we begin linking ce, which is what fixes the property-hook + * corruption seen in the earlier inline attempt. + * + * Skip when ce is itself a synthesized monomorph (its name carries `<`, + * which is invalid in user class names) — in that case the side-table + * extends entry exists for substitution, not for redirection. */ + if (ce->parent_name && ce->generic_types && ce->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends) + && !zend_class_is_monomorph(ce) + && !zend_type_contains_type_parameter(*ce->generic_types->extends)) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + zend_class_entry *base = zend_fetch_class_by_name( + ce->parent_name, lc_parent_name, + ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION); + if (!base) { + check_unrecoverable_load_failure(ce); + return NULL; + } + if (base->generic_parameters) { + /* Validate arity and bounds against the base's parameters first + * (so the user gets the contextual "extends X in Y" error wording + * rather than a generic synthesizer error). */ + zend_check_generic_link_arity(base, nwa->count, "extends", ce->name); + zend_check_generic_link_bounds(base, ce->generic_types->extends, + "extends", ce); + if (EG(exception)) { + check_unrecoverable_load_failure(ce); + return NULL; + } + zend_class_entry *mono = zend_synthesize_monomorph(base, nwa->args, nwa->count); + if (!mono) { + check_unrecoverable_load_failure(ce); + return NULL; + } + zend_string_release(ce->parent_name); + ce->parent_name = zend_string_copy(mono->name); + synthesized_lc_parent = zend_string_tolower(mono->name); + lc_parent_name = synthesized_lc_parent; + } + } + if (ce->parent_name) { parent = zend_fetch_class_by_name( ce->parent_name, lc_parent_name, ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION); if (!parent) { check_unrecoverable_load_failure(ce); + if (synthesized_lc_parent) zend_string_release(synthesized_lc_parent); return NULL; } UPDATE_IS_CACHEABLE(parent); } + if (synthesized_lc_parent) { + zend_string_release(synthesized_lc_parent); + synthesized_lc_parent = NULL; + } if (ce->num_traits || ce->num_interfaces) { traits_and_interfaces = do_alloca(sizeof(zend_class_entry*) * (ce->num_traits + ce->num_interfaces), use_heap); + const HashTable *trait_uses_table_for_synth = + (ce->generic_types && ce->generic_types->trait_uses) + ? ce->generic_types->trait_uses : NULL; + bool ce_is_mono_for_trait = zend_class_is_monomorph(ce); + bool *trait_skip_mono = NULL; + ALLOCA_FLAG(trait_skip_use_heap) + if (trait_uses_table_for_synth && ce->num_traits > 1) { + trait_skip_mono = do_alloca(sizeof(bool) * ce->num_traits, trait_skip_use_heap); + zend_mark_duplicate_lc_names(ce->trait_names, ce->num_traits, trait_skip_mono); + } + for (i = 0; i < ce->num_traits; i++) { + /* Bound-erased generics, trait-use-with-args: if the use clause is + * `use Foo;`, synthesize the Foo mono trait now and + * rewrite trait_names[i] to the canonical name. Skip when ce is + * itself a mono, when args carry a type-parameter ref (still + * symbolic), or when the same base trait is used multiple times + * with different args (diamond fallback). */ + const zend_type *trait_args = trait_uses_table_for_synth + ? (const zend_type *) zend_hash_index_find_ptr(trait_uses_table_for_synth, i) + : NULL; + if (!ce_is_mono_for_trait && trait_args + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*trait_args) + && !zend_type_contains_type_parameter(*trait_args) + && !(trait_skip_mono && trait_skip_mono[i])) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*trait_args); + zend_class_entry *base_trait = zend_fetch_class_by_name( + ce->trait_names[i].name, ce->trait_names[i].lc_name, + ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); + if (UNEXPECTED(base_trait == NULL)) { + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + return NULL; + } + if (UNEXPECTED(!(base_trait->ce_flags & ZEND_ACC_TRAIT))) { + zend_throw_error(NULL, "%s cannot use %s - it is not a trait", + ZSTR_VAL(ce->name), ZSTR_VAL(base_trait->name)); + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + return NULL; + } + if (base_trait->generic_parameters) { + zend_check_generic_link_arity(base_trait, nwa->count, "use", ce->name); + zend_check_generic_link_bounds(base_trait, trait_args, "use", ce); + if (EG(exception)) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + return NULL; + } + zend_class_entry *mono = zend_synthesize_monomorph( + base_trait, nwa->args, nwa->count); + if (!mono) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + return NULL; + } + zend_string_release(ce->trait_names[i].name); + zend_string_release(ce->trait_names[i].lc_name); + ce->trait_names[i].name = zend_string_copy(mono->name); + ce->trait_names[i].lc_name = zend_string_tolower(mono->name); + } + } + zend_class_entry *trait = zend_fetch_class_by_name(ce->trait_names[i].name, ce->trait_names[i].lc_name, ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); if (UNEXPECTED(trait == NULL)) { free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); return NULL; } if (UNEXPECTED(!(trait->ce_flags & ZEND_ACC_TRAIT))) { zend_throw_error(NULL, "%s cannot use %s - it is not a trait", ZSTR_VAL(ce->name), ZSTR_VAL(trait->name)); free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); return NULL; } if (UNEXPECTED(trait->ce_flags & ZEND_ACC_DEPRECATED)) { zend_use_of_deprecated_trait(trait, ce->name); if (UNEXPECTED(EG(exception))) { free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); return NULL; } } @@ -5604,10 +5785,73 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string UPDATE_IS_CACHEABLE(trait); } } + if (trait_skip_mono) { + free_alloca(trait_skip_mono, trait_skip_use_heap); + } } if (ce->num_interfaces) { + const HashTable *impl_table = (ce->generic_types && ce->generic_types->implements) + ? ce->generic_types->implements : NULL; + bool ce_is_mono = zend_class_is_monomorph(ce); + bool *skip_mono = NULL; + ALLOCA_FLAG(skip_mono_use_heap) + if (impl_table && ce->num_interfaces > 1) { + skip_mono = do_alloca(sizeof(bool) * ce->num_interfaces, skip_mono_use_heap); + zend_mark_duplicate_lc_names(ce->interface_names, ce->num_interfaces, skip_mono); + } for (i = 0; i < ce->num_interfaces; i++) { + /* Bound-erased generics, implements-with-args: if this interface was + * declared as `implements Iter`, the side-table holds the + * pre-erasure args. Synthesize Iter as a sequential top-level + * link, then rewrite ce->interface_names[i] to the canonical name + * so the existing fetch+inheritance machinery sees the monomorph as + * the implemented interface (and `$obj instanceof Iter` + * resolves correctly via the inheritance chain). Skip when ce is + * itself a monomorph or when args contain a type-parameter ref. */ + const zend_type *impl_args = impl_table + ? (const zend_type *) zend_hash_index_find_ptr(impl_table, i) + : NULL; + if (!ce_is_mono && impl_args && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*impl_args) + && !zend_type_contains_type_parameter(*impl_args) + && !(skip_mono && skip_mono[i])) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*impl_args); + zend_class_entry *base_iface = zend_fetch_class_by_name( + ce->interface_names[i].name, ce->interface_names[i].lc_name, + ZEND_FETCH_CLASS_INTERFACE | ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED + | ZEND_FETCH_CLASS_EXCEPTION); + if (!base_iface) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + return NULL; + } + if (base_iface->generic_parameters) { + const char *clause = (ce->ce_flags & ZEND_ACC_INTERFACE) + ? "extends" : "implements"; + zend_check_generic_link_arity(base_iface, nwa->count, + clause, ce->name); + zend_check_generic_link_bounds(base_iface, impl_args, + clause, ce); + if (EG(exception)) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + return NULL; + } + zend_class_entry *mono = zend_synthesize_monomorph( + base_iface, nwa->args, nwa->count); + if (!mono) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + return NULL; + } + zend_string_release(ce->interface_names[i].name); + zend_string_release(ce->interface_names[i].lc_name); + ce->interface_names[i].name = zend_string_copy(mono->name); + ce->interface_names[i].lc_name = zend_string_tolower(mono->name); + } + } + zend_class_entry *iface = zend_fetch_class_by_name( ce->interface_names[i].name, ce->interface_names[i].lc_name, ZEND_FETCH_CLASS_INTERFACE | @@ -5622,6 +5866,9 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string UPDATE_IS_CACHEABLE(iface); } } + if (skip_mono) { + free_alloca(skip_mono, skip_mono_use_heap); + } } zend_validate_generic_inheritance_arities(ce, parent, traits_and_interfaces); @@ -5989,6 +6236,37 @@ ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_ return ce; } + /* Bound-erased generics, extends-with-args: same redirect as in + * zend_do_link_class but for the early-binding path. Validate arity + * against the base's parameter list (so we keep the original "Too + * few/many type arguments to extends" error wording), then synthesize + * the canonical monomorph as a completed top-level link before ce's + * link begins, then rewrite parent_ce (local) and ce->parent_name to + * point at the monomorph. Skip when any arg contains a type-parameter + * ref (e.g. `class Derived extends Base`) — the args are still + * symbolic and would only become concrete when Derived itself is + * monomorphized. */ + if (parent_ce && parent_ce->generic_parameters + && ce->generic_types && ce->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends) + && !zend_class_is_monomorph(ce) + && !zend_type_contains_type_parameter(*ce->generic_types->extends)) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + zend_check_generic_link_arity(parent_ce, nwa->count, "extends", ce->name); + zend_check_generic_link_bounds(parent_ce, ce->generic_types->extends, + "extends", ce); + zend_class_entry *mono = zend_synthesize_monomorph(parent_ce, nwa->args, nwa->count); + if (!mono) { + return NULL; + } + if (ce->parent_name) { + zend_string_release(ce->parent_name); + } + ce->parent_name = zend_string_copy(mono->name); + parent_ce = mono; + } + uint32_t is_cacheable = ce->ce_flags & ZEND_ACC_IMMUTABLE; UPDATE_IS_CACHEABLE(parent_ce); if (is_cacheable) { @@ -6105,3 +6383,571 @@ ZEND_API zend_inheritance_status zend_check_generic_arg_satisfies_bound( return zend_perform_covariant_type_check(arg_scope, arg, bound_scope, bound); } + +/* === Monomorph synthesis === + * + * Builds a real class_entry for a generic application like `Box` and + * registers it in EG(class_table) under the canonical name. The synthesized + * class extends the base with the supplied type args; inheritance does the + * rest (substituted arg_info via the TRAIT_CLONE machinery, substituted + * property types, etc.). */ + +static zend_type zend_monomorph_dup_type(zend_type t) +{ + zend_type_copy_ctor(&t, /* use_arena */ false, /* persistent */ false); + return t; +} + +static zend_type zend_monomorph_build_extends_payload( + zend_class_entry *base, const zend_type *args, uint32_t arity) +{ + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(arity)); + payload->name = zend_string_copy(base->name); + payload->name_attr = 0; + payload->count = arity; + for (uint32_t i = 0; i < arity; i++) { + payload->args[i] = zend_monomorph_dup_type(args[i]); + } + zend_type result = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, payload); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + return result; +} + +ZEND_API zend_class_entry *zend_get_defaults_monomorph(zend_class_entry *base) +{ + if (!base->generic_parameters) { + return base; + } + if (EXPECTED(base->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS)) { + return zend_synthesize_monomorph(base, NULL, 0); + } + uint32_t total = base->generic_parameters->count; + for (uint32_t i = 0; i < total; i++) { + if (!ZEND_TYPE_IS_SET(base->generic_parameters->parameters[i].default_type)) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments; " + "type parameter %s has no default", + ZSTR_VAL(base->name), + ZSTR_VAL(base->generic_parameters->parameters[i].name)); + return NULL; + } + } + ZEND_UNREACHABLE(); +} + +ZEND_API zend_class_entry *zend_synthesize_monomorph( + zend_class_entry *base, const zend_type *args, uint32_t arity) +{ + if (!base->generic_parameters) { + zend_throw_error(NULL, + "Cannot monomorphize non-generic class %s", + ZSTR_VAL(base->name)); + return NULL; + } + uint32_t total = base->generic_parameters->count; + if (arity > total) { + zend_throw_error(NULL, + "Cannot monomorphize %s: expected at most %u type argument(s), got %u", + ZSTR_VAL(base->name), total, arity); + return NULL; + } + /* Fill in defaults for any trailing parameters not supplied. */ + zend_type filled[ZEND_GENERIC_MAX_PARAMS]; + if (arity < total) { + for (uint32_t i = 0; i < arity; i++) { + filled[i] = args[i]; + } + for (uint32_t i = arity; i < total; i++) { + const zend_generic_parameter *p = &base->generic_parameters->parameters[i]; + if (!ZEND_TYPE_IS_SET(p->default_type)) { + zend_throw_error(NULL, + "Cannot monomorphize %s: type parameter %s has no default and was not supplied", + ZSTR_VAL(base->name), ZSTR_VAL(p->name)); + return NULL; + } + filled[i] = p->default_type; + } + args = filled; + arity = total; + } + + zend_string *canonical = zend_generic_canonical_class_name(base->name, args, arity); + zend_string *lc_canonical = zend_string_tolower(canonical); + + zend_class_entry *existing = zend_hash_find_ptr(EG(class_table), lc_canonical); + if (existing) { + zend_string_release(canonical); + zend_string_release(lc_canonical); + return existing; + } + + /* Validate bounds before committing to a class entry. */ + zend_type extends_payload = zend_monomorph_build_extends_payload(base, args, arity); + { + zend_class_entry tmp_scope; + memset(&tmp_scope, 0, sizeof(tmp_scope)); + tmp_scope.name = canonical; + tmp_scope.type = ZEND_USER_CLASS; + zend_check_generic_link_bounds(base, &extends_payload, "new", &tmp_scope); + if (EG(exception)) { + zend_type_release(extends_payload, /* persistent */ false); + zend_string_release(canonical); + zend_string_release(lc_canonical); + return NULL; + } + } + + zend_class_entry *ce = zend_arena_alloc(&CG(arena), sizeof(zend_class_entry)); + ce->type = ZEND_USER_CLASS; + zend_initialize_class_data(ce, /* nullify_handlers */ true); + ce->ce_flags &= ~ZEND_ACC_IMMUTABLE; + ce->ce_flags |= ZEND_ACC_TOP_LEVEL; + /* Carry the base's class-shape flags (interface, trait, enum) so the + * monomorph is the same kind of class-like as the base. Without this, + * an interface base would synthesize a class trying to extend the + * interface, which the linker rejects. */ + ce->ce_flags |= base->ce_flags & (ZEND_ACC_INTERFACE | ZEND_ACC_TRAIT | ZEND_ACC_ENUM); + ce->info.user.filename = base->info.user.filename + ? zend_string_copy(base->info.user.filename) + : ZSTR_EMPTY_ALLOC(); + ce->info.user.line_start = base->info.user.line_start; + ce->info.user.line_end = base->info.user.line_end; + ce->name = canonical; + + bool base_is_interface = (base->ce_flags & ZEND_ACC_INTERFACE) != 0; + bool base_is_trait = (base->ce_flags & ZEND_ACC_TRAIT) != 0; + if (base_is_interface) { + /* Interface inheritance in PHP goes through `interface_names[]`, not + * `parent_name`. Wire the mono to the base interface via one entry in + * `interfaces[]` and store the substitution args in the `implements` + * side-table so the inheritance pipeline can substitute T → arg in + * the inherited methods. */ + ce->num_interfaces = 1; + ce->interface_names = emalloc(sizeof(zend_class_name)); + ce->interface_names[0].name = zend_string_copy(base->name); + ce->interface_names[0].lc_name = zend_string_tolower(base->name); + zend_generic_type_table_set_implements( + zend_generic_get_or_create_class_table(ce), 0, extends_payload); + } else if (base_is_trait) { + /* Trait composition in PHP goes through `trait_names[]`/`num_traits`. + * The mono `uses` the base trait with the substitution args recorded + * in `trait_uses` so the trait-method import path picks them up. */ + ce->num_traits = 1; + ce->trait_names = emalloc(sizeof(zend_class_name)); + ce->trait_names[0].name = zend_string_copy(base->name); + ce->trait_names[0].lc_name = zend_string_tolower(base->name); + zend_generic_type_table_set_trait_use( + zend_generic_get_or_create_class_table(ce), 0, extends_payload); + } else { + ce->parent_name = zend_string_copy(base->name); + zend_generic_type_table_set_extends( + zend_generic_get_or_create_class_table(ce), extends_payload); + } + + zval ce_zv; + ZVAL_PTR(&ce_zv, ce); + if (zend_hash_add(EG(class_table), lc_canonical, &ce_zv) == NULL) { + /* Race: another path registered this canonical name first. */ + zend_class_entry *winner = zend_hash_find_ptr(EG(class_table), lc_canonical); + destroy_zend_class(&ce_zv); + zend_string_release(lc_canonical); + return winner; + } + + zend_string *parent_lc = (base_is_interface || base_is_trait) + ? NULL : zend_string_tolower(base->name); + + /* Monomorph synthesis is an engine-internal child of the base, not a + * user-declared subclass. final/readonly restrictions that gate + * user-level extension are temporarily suppressed for the link and then + * re-applied to the monomorph so user code still can't `extends Box` + * if Box itself was final. Abstract is propagated so the monomorph of an + * abstract class stays abstract (and a concrete subclass can implement + * its methods). */ + uint32_t suppressed = base->ce_flags & (ZEND_ACC_FINAL | ZEND_ACC_READONLY_CLASS); + uint32_t propagated = base->ce_flags & (ZEND_ACC_EXPLICIT_ABSTRACT_CLASS); + base->ce_flags &= ~suppressed; + ce->ce_flags |= propagated; + zend_class_entry *linked = zend_do_link_class(ce, parent_lc, lc_canonical); + base->ce_flags |= suppressed; + if (linked) { + linked->ce_flags |= suppressed; + } + if (parent_lc) zend_string_release(parent_lc); + + if (!linked) { + zend_hash_del(EG(class_table), lc_canonical); + zend_string_release(lc_canonical); + return NULL; + } + + /* Isolate static-property storage from the base class. Inheritance normally + * makes the child's `default_static_members_table` slots IS_INDIRECT back to + * the parent so that all subclasses share the same live storage. For + * monomorphs that's the wrong semantic: `Box::$count` and + * `Box::$count` should be independent counters. Convert each + * INDIRECT slot to a direct copy of the underlying default; at first access + * `zend_class_init_statics` will then allocate the monomorph its own live + * slot and `ZVAL_COPY_OR_DUP` the default into it. */ + if (linked->default_static_members_count) { + for (uint32_t i = 0; i < linked->default_static_members_count; i++) { + zval *slot = &linked->default_static_members_table[i]; + if (Z_TYPE_P(slot) == IS_INDIRECT) { + zval *src = Z_INDIRECT_P(slot); + ZVAL_DEINDIRECT(src); + ZVAL_COPY_OR_DUP(slot, src); + } + } + } + + /* Substitute T-typed class constants. When the base declares `const T FOO`, + * the inherited entry in the mono is a shared pointer to the base's + * `zend_class_constant` with type T (erased to mixed). For the mono, + * clone any entry whose pre-erasure side-table type references a generic + * parameter, and substitute T → arg so reflection and assignability + * checks see the concrete type for this monomorph. */ + if (base->generic_types && base->generic_types->class_constants) { + zend_string *cname; + zval *zv; + ZEND_HASH_FOREACH_STR_KEY_VAL(base->generic_types->class_constants, cname, zv) { + const zend_type *pre = (const zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_IS_SET(*pre) || !zend_type_contains_type_parameter(*pre)) { + continue; + } + zend_type sub = zend_substitute_leaf_type_param(*pre, args, arity); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + continue; + } + zval *cv = zend_hash_find_known_hash(&linked->constants_table, cname); + if (!cv) continue; + zend_class_constant *orig = (zend_class_constant *) Z_PTR_P(cv); + zend_class_constant *clone = zend_arena_alloc(&CG(arena), sizeof(zend_class_constant)); + memcpy(clone, orig, sizeof(zend_class_constant)); + clone->type = sub; + zend_type_copy_ctor(&clone->type, /* use_arena */ true, /* persistent */ false); + Z_PTR_P(cv) = clone; + } ZEND_HASH_FOREACH_END(); + } + + zend_string_release(lc_canonical); + return linked; +} + +/* === Monomorph name parser === + * + * Parses class-name strings like "Box>" into a base + * class name plus a type-args list. Used by the class-lookup hook so that + * unserialize, dynamic `new $name`, and `class_exists` all synthesize the + * monomorph on demand if it hasn't been materialized in this request yet. + * + * Accepts the same shape the canonicalizer produces, plus generally any + * type-expression syntax: unions with `|`, intersections with `&`, DNF + * parens, nested generics, fully-qualified namespaced names, scalar + * keywords (int, string, bool, float, array, object, callable, mixed, + * void, never, null, true, false). Whitespace is permitted. + * + * The parser is intentionally permissive about input form; the synthesizer + * canonicalizes anyway, so semantically-equivalent inputs collapse to one + * class entry. */ + +typedef struct { + const char *p; + const char *end; + bool error; +} zend_monomorph_parser; + +static void zend_mp_skip_ws(zend_monomorph_parser *s) { + while (s->p < s->end && (*s->p == ' ' || *s->p == '\t')) s->p++; +} + +static bool zend_mp_eat(zend_monomorph_parser *s, char c) { + zend_mp_skip_ws(s); + if (s->p < s->end && *s->p == c) { s->p++; return true; } + return false; +} + +static bool zend_mp_peek(zend_monomorph_parser *s, char c) { + zend_mp_skip_ws(s); + return s->p < s->end && *s->p == c; +} + +static zend_string *zend_mp_read_ident(zend_monomorph_parser *s) { + zend_mp_skip_ws(s); + const char *start = s->p; + while (s->p < s->end) { + unsigned char ch = (unsigned char)*s->p; + bool ok = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') + || (ch >= '0' && ch <= '9') || ch == '_' || ch == '\\' || ch >= 0x80; + if (!ok) break; + s->p++; + } + if (s->p == start) return NULL; + return zend_string_init(start, s->p - start, 0); +} + +static struct { const char *kw; uint32_t bit; } zend_mp_scalars[] = { + { "int", MAY_BE_LONG }, + { "string", MAY_BE_STRING }, + { "float", MAY_BE_DOUBLE }, + { "bool", MAY_BE_BOOL }, + { "true", MAY_BE_TRUE }, + { "false", MAY_BE_FALSE }, + { "null", MAY_BE_NULL }, + { "array", MAY_BE_ARRAY }, + { "object", MAY_BE_OBJECT }, + { "callable", MAY_BE_CALLABLE }, + { "void", MAY_BE_VOID }, + { "never", MAY_BE_NEVER }, + { "mixed", MAY_BE_ANY }, + { NULL, 0 } +}; + +static uint32_t zend_mp_scalar_bit(const zend_string *ident) { + for (size_t i = 0; zend_mp_scalars[i].kw; i++) { + size_t kwlen = strlen(zend_mp_scalars[i].kw); + if (ZSTR_LEN(ident) == kwlen + && zend_binary_strcasecmp( + ZSTR_VAL(ident), ZSTR_LEN(ident), + zend_mp_scalars[i].kw, kwlen) == 0) { + return zend_mp_scalars[i].bit; + } + } + return 0; +} + +static zend_type zend_mp_parse_type(zend_monomorph_parser *s); + +/* Single atomic type: scalar keyword, class name (with optional <...>), + * or a parenthesized intersection used inside a union (DNF). */ +static zend_type zend_mp_parse_atom(zend_monomorph_parser *s) +{ + zend_mp_skip_ws(s); + if (zend_mp_eat(s, '(')) { + /* DNF intersection branch */ + zend_type inner = zend_mp_parse_type(s); + if (s->error || !zend_mp_eat(s, ')')) { + s->error = true; + zend_type_release(inner, /* persistent */ false); + return (zend_type) ZEND_TYPE_INIT_NONE(0); + } + return inner; + } + + zend_string *ident = zend_mp_read_ident(s); + if (!ident) { s->error = true; return (zend_type) ZEND_TYPE_INIT_NONE(0); } + + /* Nested generic? */ + if (zend_mp_peek(s, '<')) { + s->p++; /* consume '<' */ + uint32_t cap = 4, count = 0; + zend_type *args = emalloc(sizeof(zend_type) * cap); + do { + if (count == cap) { + cap *= 2; + args = erealloc(args, sizeof(zend_type) * cap); + } + args[count++] = zend_mp_parse_type(s); + if (s->error) break; + } while (zend_mp_eat(s, ',')); + if (!s->error && !zend_mp_eat(s, '>')) s->error = true; + if (s->error) { + for (uint32_t i = 0; i < count; i++) { + zend_type_release(args[i], false); + } + efree(args); + zend_string_release(ident); + return (zend_type) ZEND_TYPE_INIT_NONE(0); + } + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(count)); + payload->name = ident; + payload->name_attr = 0; + payload->count = count; + for (uint32_t i = 0; i < count; i++) payload->args[i] = args[i]; + efree(args); + zend_type result = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, payload); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + return result; + } + + uint32_t scalar = zend_mp_scalar_bit(ident); + if (scalar) { + zend_string_release(ident); + zend_type t = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_FULL_MASK(t) = scalar; + return t; + } + return (zend_type) ZEND_TYPE_INIT_CLASS(ident, 0, 0); +} + +/* `atom (& atom)*` -- a single intersection branch. + * Returns either a single atom or an intersection list. */ +static zend_type zend_mp_parse_intersection(zend_monomorph_parser *s) +{ + zend_type first = zend_mp_parse_atom(s); + if (s->error || !zend_mp_peek(s, '&')) return first; + + uint32_t cap = 4, count = 1; + zend_type *parts = emalloc(sizeof(zend_type) * cap); + parts[0] = first; + while (zend_mp_eat(s, '&')) { + if (count == cap) { cap *= 2; parts = erealloc(parts, sizeof(zend_type) * cap); } + parts[count++] = zend_mp_parse_atom(s); + if (s->error) break; + } + if (s->error) { + for (uint32_t i = 0; i < count; i++) zend_type_release(parts[i], false); + efree(parts); + return (zend_type) ZEND_TYPE_INIT_NONE(0); + } + zend_type_list *list = emalloc(ZEND_TYPE_LIST_SIZE(count)); + list->num_types = count; + for (uint32_t i = 0; i < count; i++) list->types[i] = parts[i]; + efree(parts); + zend_type result = ZEND_TYPE_INIT_INTERSECTION(list, 0); + return result; +} + +/* `intersection ('|' intersection)*` -- top-level type expression. + * Returns either a single intersection, a single atom, or a union list. */ +static zend_type zend_mp_parse_type(zend_monomorph_parser *s) +{ + zend_type first = zend_mp_parse_intersection(s); + if (s->error || !zend_mp_peek(s, '|')) return first; + + uint32_t cap = 4, count = 1; + zend_type *parts = emalloc(sizeof(zend_type) * cap); + parts[0] = first; + while (zend_mp_eat(s, '|')) { + if (count == cap) { cap *= 2; parts = erealloc(parts, sizeof(zend_type) * cap); } + parts[count++] = zend_mp_parse_intersection(s); + if (s->error) break; + } + if (s->error) { + for (uint32_t i = 0; i < count; i++) zend_type_release(parts[i], false); + efree(parts); + return (zend_type) ZEND_TYPE_INIT_NONE(0); + } + + /* Collapse scalar-bit-only parts into the type_mask of the resulting list + * so e.g. "int|string" lands as a single zend_type with both bits set, no + * list needed. */ + uint32_t scalar_mask = 0; + uint32_t complex_count = 0; + for (uint32_t i = 0; i < count; i++) { + bool is_pure_scalar = !ZEND_TYPE_HAS_NAME(parts[i]) + && !ZEND_TYPE_HAS_LIST(parts[i]) + && !ZEND_TYPE_HAS_NAMED_WITH_ARGS(parts[i]); + if (is_pure_scalar) { + scalar_mask |= ZEND_TYPE_FULL_MASK(parts[i]); + } else { + complex_count++; + } + } + if (complex_count == 0) { + efree(parts); + zend_type t = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_FULL_MASK(t) = scalar_mask; + return t; + } + if (complex_count == 1 && scalar_mask == 0) { + zend_type only = (zend_type) ZEND_TYPE_INIT_NONE(0); + for (uint32_t i = 0; i < count; i++) { + if (ZEND_TYPE_HAS_NAME(parts[i]) || ZEND_TYPE_HAS_LIST(parts[i]) + || ZEND_TYPE_HAS_NAMED_WITH_ARGS(parts[i])) { + only = parts[i]; + break; + } + } + efree(parts); + return only; + } + zend_type_list *list = emalloc(ZEND_TYPE_LIST_SIZE(complex_count)); + list->num_types = complex_count; + uint32_t li = 0; + for (uint32_t i = 0; i < count; i++) { + bool is_pure_scalar = !ZEND_TYPE_HAS_NAME(parts[i]) + && !ZEND_TYPE_HAS_LIST(parts[i]) + && !ZEND_TYPE_HAS_NAMED_WITH_ARGS(parts[i]); + if (!is_pure_scalar) { + list->types[li++] = parts[i]; + } + } + efree(parts); + zend_type result = ZEND_TYPE_INIT_UNION(list, 0); + ZEND_TYPE_FULL_MASK(result) |= scalar_mask; + return result; +} + +/* Find the matching `>` for the outermost `<` in `name`. Returns the offset + * of `<` (in bytes from the start) and the length of the args span. Returns + * false if `name` doesn't have generic shape or has unbalanced angles. */ +static bool zend_mp_split_name( + const zend_string *name, size_t *out_lt_pos, size_t *out_args_len) +{ + const char *s = ZSTR_VAL(name); + size_t n = ZSTR_LEN(name); + if (n < 4) return false; /* shortest possible: X */ + if (s[n - 1] != '>') return false; + + int depth = 0; + size_t lt = SIZE_MAX; + for (size_t i = 0; i < n; i++) { + if (s[i] == '<') { + if (depth == 0) lt = i; + depth++; + } else if (s[i] == '>') { + depth--; + if (depth == 0 && i != n - 1) return false; /* trailing data after closing */ + } + } + if (depth != 0 || lt == SIZE_MAX || lt == 0) return false; + *out_lt_pos = lt; + *out_args_len = n - lt - 2; /* exclude '<' and '>' */ + return true; +} + +ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( + zend_string *name, uint32_t flags) +{ + size_t lt_pos, args_len; + if (!zend_mp_split_name(name, <_pos, &args_len)) return NULL; + + zend_string *base_name = zend_string_init(ZSTR_VAL(name), lt_pos, 0); + + /* Recursively look up the base class. If the base is itself a monomorph + * name, the lookup hook will be reentrant and synthesize it first. */ + uint32_t lookup_flags = flags | ZEND_FETCH_CLASS_NO_AUTOLOAD; + zend_class_entry *base = zend_lookup_class_ex(base_name, NULL, lookup_flags); + if (!base && !(flags & ZEND_FETCH_CLASS_NO_AUTOLOAD)) { + /* Allow autoload for the base name only, in case it isn't loaded yet. */ + base = zend_lookup_class_ex(base_name, NULL, flags & ~ZEND_FETCH_CLASS_NO_AUTOLOAD); + } + zend_string_release(base_name); + if (!base || !base->generic_parameters) return NULL; + + zend_monomorph_parser parser = { + .p = ZSTR_VAL(name) + lt_pos + 1, + .end = ZSTR_VAL(name) + ZSTR_LEN(name) - 1, + .error = false, + }; + uint32_t cap = 4, count = 0; + zend_type *args = emalloc(sizeof(zend_type) * cap); + do { + if (count == cap) { cap *= 2; args = erealloc(args, sizeof(zend_type) * cap); } + args[count++] = zend_mp_parse_type(&parser); + if (parser.error) break; + } while (zend_mp_eat(&parser, ',')); + zend_mp_skip_ws(&parser); + if (parser.error || parser.p != parser.end) { + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return NULL; + } + + zend_class_entry *mono = zend_synthesize_monomorph(base, args, count); + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return mono; +} diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index 5bd085e90cea..ff214ceab4db 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -58,6 +58,47 @@ static zend_always_inline void zend_do_inheritance(zend_class_entry *ce, zend_cl ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name, const zend_string *key); +/* Monomorphization: synthesize a generic class application as a real + * class_entry registered in EG(class_table) under the canonical name. The + * synthesized class extends `base` with the supplied type args, inheriting + * all methods/properties/constants with proper substitution. + * + * Idempotent: a second call with the same (base, args) canonicalizing to an + * existing entry returns that entry. Returns NULL on failure (exception will + * be set). The args array is copied; the caller retains ownership. */ +ZEND_API zend_class_entry *zend_synthesize_monomorph( + zend_class_entry *base, const zend_type *args, uint32_t arity); + +/* For a bare generic class `base`, synthesize (or return the cached) monomorph + * built from the parameters' declared defaults. Returns NULL and throws Error + * if any parameter has no default. If `base` is itself a monomorph (no + * generic_parameters), returns `base` unchanged. Used by ZEND_NEW for the + * `new static()` and dynamic `new $name()` paths. */ +ZEND_API zend_class_entry *zend_get_defaults_monomorph(zend_class_entry *base); + +/* True when the name has monomorph-canonical shape (contains `<`). The canonical + * encoding for synthesized monomorphs embeds `<...>` in the class name, which is + * invalid in any user-declared class. Use these helpers rather than open-coding + * the memchr check so the encoding stays in one place. */ +static zend_always_inline bool zend_class_name_is_monomorph(const zend_string *name) +{ + return memchr(ZSTR_VAL(name), '<', ZSTR_LEN(name)) != NULL; +} + +static zend_always_inline bool zend_class_is_monomorph(const zend_class_entry *ce) +{ + return zend_class_name_is_monomorph(ce->name); +} + +/* Parses a generic-shaped class-name string ("Box", etc.), looks + * up the base class, and synthesizes the monomorph. Returns NULL if the + * name doesn't have generic shape, the base isn't generic, or parsing fails. + * Used by `zend_lookup_class_ex` to make unserialize, dynamic + * `new $name()`, and `class_exists()` all transparently materialize + * monomorphs on demand. */ +ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( + zend_string *name, uint32_t flags); + void zend_verify_abstract_class(zend_class_entry *ce); void zend_build_properties_info_table(zend_class_entry *ce); ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_entry *parent_ce, zend_string *lcname, zval *delayed_early_binding); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 625590219104..d12f17da2094 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6020,6 +6020,38 @@ ZEND_VM_HANDLER(68, ZEND_NEW, UNUSED|CLASS_FETCH|CONST|VAR, UNUSED|CACHE_SLOT, N ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class (via `new static()`, `new $name`, + * or any path that didn't go through compile-time canonical-name rewrite). + * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles + * the synthesis-and-swap for the turbofish path. Otherwise: if every + * type parameter has a default, synthesize and use the defaults monomorph. + * For `new static()` / lexical paths with no defaults, fall back to a + * bare instance (preserves the lexical-self semantic for generic classes + * whose authors didn't declare defaults). For dynamic `new $name()` we + * throw — the caller spelled out a generic class by name and a bare + * instance with erased T is almost never what they want. + * + * OP1_TYPE distinguishes: IS_UNUSED → static/self/parent (lenient); + * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ + if (UNEXPECTED(OP1_TYPE != IS_CONST && ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else if (OP1_TYPE == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + /* IS_UNUSED with no defaults: fall through with the bare ce. */ + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -8962,6 +8994,23 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); zend_check_generic_new_arguments(ce, arity, args_box); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. */ + if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } } if (UNEXPECTED(EG(exception))) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 392cb4af61e5..c941c4ddd4ab 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -11413,6 +11413,38 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_CONS ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class (via `new static()`, `new $name`, + * or any path that didn't go through compile-time canonical-name rewrite). + * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles + * the synthesis-and-swap for the turbofish path. Otherwise: if every + * type parameter has a default, synthesize and use the defaults monomorph. + * For `new static()` / lexical paths with no defaults, fall back to a + * bare instance (preserves the lexical-self semantic for generic classes + * whose authors didn't declare defaults). For dynamic `new $name()` we + * throw — the caller spelled out a generic class by name and a bare + * instance with erased T is almost never what they want. + * + * IS_CONST distinguishes: IS_UNUSED → static/self/parent (lenient); + * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ + if (UNEXPECTED(IS_CONST != IS_CONST && ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else if (IS_CONST == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + /* IS_UNUSED with no defaults: fall through with the bare ce. */ + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -22025,6 +22057,23 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); zend_check_generic_new_arguments(ce, arity, args_box); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. */ + if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } } if (UNEXPECTED(EG(exception))) { @@ -30130,6 +30179,38 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_VAR_ ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class (via `new static()`, `new $name`, + * or any path that didn't go through compile-time canonical-name rewrite). + * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles + * the synthesis-and-swap for the turbofish path. Otherwise: if every + * type parameter has a default, synthesize and use the defaults monomorph. + * For `new static()` / lexical paths with no defaults, fall back to a + * bare instance (preserves the lexical-self semantic for generic classes + * whose authors didn't declare defaults). For dynamic `new $name()` we + * throw — the caller spelled out a generic class by name and a bare + * instance with erased T is almost never what they want. + * + * IS_VAR distinguishes: IS_UNUSED → static/self/parent (lenient); + * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ + if (UNEXPECTED(IS_VAR != IS_CONST && ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else if (IS_VAR == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + /* IS_UNUSED with no defaults: fall through with the bare ce. */ + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -37178,6 +37259,38 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_UNUS ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class (via `new static()`, `new $name`, + * or any path that didn't go through compile-time canonical-name rewrite). + * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles + * the synthesis-and-swap for the turbofish path. Otherwise: if every + * type parameter has a default, synthesize and use the defaults monomorph. + * For `new static()` / lexical paths with no defaults, fall back to a + * bare instance (preserves the lexical-self semantic for generic classes + * whose authors didn't declare defaults). For dynamic `new $name()` we + * throw — the caller spelled out a generic class by name and a bare + * instance with erased T is almost never what they want. + * + * IS_UNUSED distinguishes: IS_UNUSED → static/self/parent (lenient); + * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ + if (UNEXPECTED(IS_UNUSED != IS_CONST && ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else if (IS_UNUSED == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + /* IS_UNUSED with no defaults: fall through with the bare ce. */ + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -37377,6 +37490,23 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); zend_check_generic_new_arguments(ce, arity, args_box); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. */ + if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } } if (UNEXPECTED(EG(exception))) { @@ -64086,6 +64216,38 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_CONST_UNU ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class (via `new static()`, `new $name`, + * or any path that didn't go through compile-time canonical-name rewrite). + * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles + * the synthesis-and-swap for the turbofish path. Otherwise: if every + * type parameter has a default, synthesize and use the defaults monomorph. + * For `new static()` / lexical paths with no defaults, fall back to a + * bare instance (preserves the lexical-self semantic for generic classes + * whose authors didn't declare defaults). For dynamic `new $name()` we + * throw — the caller spelled out a generic class by name and a bare + * instance with erased T is almost never what they want. + * + * IS_CONST distinguishes: IS_UNUSED → static/self/parent (lenient); + * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ + if (UNEXPECTED(IS_CONST != IS_CONST && ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else if (IS_CONST == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + /* IS_UNUSED with no defaults: fall through with the bare ce. */ + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -74598,6 +74760,23 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); zend_check_generic_new_arguments(ce, arity, args_box); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. */ + if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } } if (UNEXPECTED(EG(exception))) { @@ -82703,6 +82882,38 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_VAR_UNUSE ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class (via `new static()`, `new $name`, + * or any path that didn't go through compile-time canonical-name rewrite). + * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles + * the synthesis-and-swap for the turbofish path. Otherwise: if every + * type parameter has a default, synthesize and use the defaults monomorph. + * For `new static()` / lexical paths with no defaults, fall back to a + * bare instance (preserves the lexical-self semantic for generic classes + * whose authors didn't declare defaults). For dynamic `new $name()` we + * throw — the caller spelled out a generic class by name and a bare + * instance with erased T is almost never what they want. + * + * IS_VAR distinguishes: IS_UNUSED → static/self/parent (lenient); + * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ + if (UNEXPECTED(IS_VAR != IS_CONST && ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else if (IS_VAR == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + /* IS_UNUSED with no defaults: fall through with the bare ce. */ + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -89751,6 +89962,38 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_UNUSED_UN ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class (via `new static()`, `new $name`, + * or any path that didn't go through compile-time canonical-name rewrite). + * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles + * the synthesis-and-swap for the turbofish path. Otherwise: if every + * type parameter has a default, synthesize and use the defaults monomorph. + * For `new static()` / lexical paths with no defaults, fall back to a + * bare instance (preserves the lexical-self semantic for generic classes + * whose authors didn't declare defaults). For dynamic `new $name()` we + * throw — the caller spelled out a generic class by name and a bare + * instance with erased T is almost never what they want. + * + * IS_UNUSED distinguishes: IS_UNUSED → static/self/parent (lenient); + * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ + if (UNEXPECTED(IS_UNUSED != IS_CONST && ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else if (IS_UNUSED == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + /* IS_UNUSED with no defaults: fall through with the bare ce. */ + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -89950,6 +90193,23 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); zend_check_generic_new_arguments(ce, arity, args_box); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. */ + if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } } if (UNEXPECTED(EG(exception))) { diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 166c3064eb19..680ea95bc266 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -124,6 +124,19 @@ PHPAPI zend_class_entry *reflection_property_hook_type_ptr; target = intern->ptr; \ } while (0) +/* When reflecting a bare generic class, swap in the defaults monomorph (same + * contract as `new ClassName()`); throws and returns from the caller if any + * type parameter has no default. Reflecting an existing monomorph leaves + * `ce` unchanged. */ +#define REFLECTION_RESOLVE_GENERIC_DEFAULTS(ce) do { \ + if ((ce)->generic_parameters) { \ + (ce) = zend_get_defaults_monomorph(ce); \ + if (UNEXPECTED((ce) == NULL)) { \ + RETURN_THROWS(); \ + } \ + } \ +} while (0) + /* {{{ Object structure */ /* Struct for properties */ @@ -5283,6 +5296,8 @@ ZEND_METHOD(ReflectionClass, newInstance) GET_REFLECTION_OBJECT_PTR(ce); + REFLECTION_RESOLVE_GENERIC_DEFAULTS(ce); + if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { return; } @@ -5337,6 +5352,8 @@ ZEND_METHOD(ReflectionClass, newInstanceWithoutConstructor) RETURN_THROWS(); } + REFLECTION_RESOLVE_GENERIC_DEFAULTS(ce); + object_init_ex(return_value, ce); } /* }}} */ @@ -5360,6 +5377,8 @@ ZEND_METHOD(ReflectionClass, newInstanceArgs) argc = zend_hash_num_elements(args); } + REFLECTION_RESOLVE_GENERIC_DEFAULTS(ce); + if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { return; } From c1a1cf2caee137585cb0fffa24e99d48237ed12b Mon Sep 17 00:00:00 2001 From: Rob Landers Date: Tue, 19 May 2026 17:38:31 +0200 Subject: [PATCH 04/30] reify method/functions --- .../errors/type_param_in_class_const.phpt | 5 +- .../errors/type_param_in_instanceof.phpt | 9 +- .../errors/type_param_in_new_expression.phpt | 11 +- .../errors/type_param_in_static_call.phpt | 9 +- .../reification/T_keyed_access_forms.phpt | 54 ++++ .../generics/reification/bound_fallback.phpt | 27 ++ .../reification/closure_captures_T.phpt | 21 ++ .../reification/func_get_type_args.phpt | 34 +++ .../reification/generator_preserves_T.phpt | 26 ++ .../generics/reification/inference_basic.phpt | 21 ++ .../interface_dispatched_method.phpt | 27 ++ .../generics/reification/method_level_T.phpt | 20 ++ .../multiple_inference_targets_agree.phpt | 24 ++ .../recursion_per_frame_table.phpt | 43 +++ .../turbofish_chains_through_outer.phpt | 21 ++ .../generics/reification/turbofish_new_T.phpt | 17 ++ .../file_scope_class_does_not_shadow.phpt | 16 +- Zend/zend_builtin_functions.c | 41 +++ Zend/zend_builtin_functions.stub.php | 6 + Zend/zend_builtin_functions_arginfo.h | 10 +- Zend/zend_compile.c | 251 ++++++++++++++++-- Zend/zend_compile.h | 31 +++ Zend/zend_execute.c | 4 + Zend/zend_execute.h | 6 + Zend/zend_execute_API.c | 35 +++ Zend/zend_generators.c | 9 + Zend/zend_opcode.c | 31 +++ Zend/zend_vm_def.h | 30 +++ Zend/zend_vm_execute.h | 142 ++++++++++ 29 files changed, 937 insertions(+), 44 deletions(-) create mode 100644 Zend/tests/generics/reification/T_keyed_access_forms.phpt create mode 100644 Zend/tests/generics/reification/bound_fallback.phpt create mode 100644 Zend/tests/generics/reification/closure_captures_T.phpt create mode 100644 Zend/tests/generics/reification/func_get_type_args.phpt create mode 100644 Zend/tests/generics/reification/generator_preserves_T.phpt create mode 100644 Zend/tests/generics/reification/inference_basic.phpt create mode 100644 Zend/tests/generics/reification/interface_dispatched_method.phpt create mode 100644 Zend/tests/generics/reification/method_level_T.phpt create mode 100644 Zend/tests/generics/reification/multiple_inference_targets_agree.phpt create mode 100644 Zend/tests/generics/reification/recursion_per_frame_table.phpt create mode 100644 Zend/tests/generics/reification/turbofish_chains_through_outer.phpt create mode 100644 Zend/tests/generics/reification/turbofish_new_T.phpt diff --git a/Zend/tests/generics/errors/type_param_in_class_const.phpt b/Zend/tests/generics/errors/type_param_in_class_const.phpt index 7db77b73d862..fc8c6410544a 100644 --- a/Zend/tests/generics/errors/type_param_in_class_const.phpt +++ b/Zend/tests/generics/errors/type_param_in_class_const.phpt @@ -1,10 +1,11 @@ --TEST-- -Errors: type parameter cannot be used at runtime in `T::class` +Errors: bare function-level type parameter used in `T::class` errors at runtime when nothing pins it --FILE-- (): string { return T::class; } +f(); ?> --EXPECTF-- -Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d +Fatal error: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_instanceof.phpt b/Zend/tests/generics/errors/type_param_in_instanceof.phpt index 34b51546cda1..45644edbe8d4 100644 --- a/Zend/tests/generics/errors/type_param_in_instanceof.phpt +++ b/Zend/tests/generics/errors/type_param_in_instanceof.phpt @@ -1,10 +1,15 @@ --TEST-- -Errors: type parameter cannot be used at runtime in `instanceof` +Errors: bare function-level type parameter used in `instanceof T` errors at runtime when nothing pins it --FILE-- ($x): bool { return $x instanceof T; } +f(new stdClass()); ?> --EXPECTF-- -Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d +Fatal error: Uncaught Error: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class in %s:%d +Stack trace: +#0 %s(%d): f(Object(stdClass)) +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_new_expression.phpt b/Zend/tests/generics/errors/type_param_in_new_expression.phpt index f10f9ace66e0..bd7a6bfc8d8b 100644 --- a/Zend/tests/generics/errors/type_param_in_new_expression.phpt +++ b/Zend/tests/generics/errors/type_param_in_new_expression.phpt @@ -1,10 +1,15 @@ --TEST-- -Errors: type parameter cannot be used at runtime as a class in `new T()` +Errors: bare function-level type parameter used in `new T()` errors at runtime when nothing pins it --FILE-- (): void { - $x = new T(); + new T(); } +f(); ?> --EXPECTF-- -Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d +Fatal error: Uncaught Error: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class in %s:%d +Stack trace: +#0 %s(%d): f() +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_call.phpt b/Zend/tests/generics/errors/type_param_in_static_call.phpt index ed23a33db626..66cd5ae9cab7 100644 --- a/Zend/tests/generics/errors/type_param_in_static_call.phpt +++ b/Zend/tests/generics/errors/type_param_in_static_call.phpt @@ -1,10 +1,15 @@ --TEST-- -Errors: type parameter cannot be used at runtime as a static-call target +Errors: bare function-level type parameter used as a static-call target errors at runtime when nothing pins it --FILE-- (): void { T::foo(); } +f(); ?> --EXPECTF-- -Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d +Fatal error: Uncaught Error: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class in %s:%d +Stack trace: +#0 %s(%d): f() +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/generics/reification/T_keyed_access_forms.phpt b/Zend/tests/generics/reification/T_keyed_access_forms.phpt new file mode 100644 index 000000000000..87a961137ab2 --- /dev/null +++ b/Zend/tests/generics/reification/T_keyed_access_forms.phpt @@ -0,0 +1,54 @@ +--TEST-- +Reification: every T-keyed expression form resolves through the frame's T-table +--FILE-- +($x): array { + return [ + "new T()" => get_class(new T()), + "T::NAME" => T::NAME, + "T::class" => T::class, + "T::make()" => get_class(T::make()), + "instanceof"=> $x instanceof T, + ]; +} + +var_dump(show::(new Foo())); +var_dump(show::(new Foo())); +?> +--EXPECT-- +array(5) { + ["new T()"]=> + string(3) "Foo" + ["T::NAME"]=> + string(3) "Foo" + ["T::class"]=> + string(3) "Foo" + ["T::make()"]=> + string(3) "Foo" + ["instanceof"]=> + bool(true) +} +array(5) { + ["new T()"]=> + string(3) "Bar" + ["T::NAME"]=> + string(3) "Bar" + ["T::class"]=> + string(3) "Bar" + ["T::make()"]=> + string(3) "Bar" + ["instanceof"]=> + bool(false) +} diff --git a/Zend/tests/generics/reification/bound_fallback.phpt b/Zend/tests/generics/reification/bound_fallback.phpt new file mode 100644 index 000000000000..3021dd02ac12 --- /dev/null +++ b/Zend/tests/generics/reification/bound_fallback.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: when no binding is supplied, the parameter's class bound is the fallback +--FILE-- +(): T { + // No turbofish, no default, no inferable arg. T falls back to its bound. + return new T(); +} + +var_dump(makeDefault()->kind); + +// Inference still wins over the bound. +function makeFromArg(T $hint): T { + return new T(); +} +var_dump(makeFromArg(new Derived())->kind); + +// Turbofish still wins over the bound. +var_dump(makeDefault::()->kind); +?> +--EXPECT-- +string(4) "base" +string(7) "derived" +string(7) "derived" diff --git a/Zend/tests/generics/reification/closure_captures_T.phpt b/Zend/tests/generics/reification/closure_captures_T.phpt new file mode 100644 index 000000000000..60ff11ed5d5a --- /dev/null +++ b/Zend/tests/generics/reification/closure_captures_T.phpt @@ -0,0 +1,21 @@ +--TEST-- +Reification: a closure returned from a generic function captures its T via an eagerly-resolved variable +--FILE-- +(): Closure { + $name = T::class; + return fn() => $name; +} + +echo fact::()(), "\n"; +echo fact::()(), "\n"; +?> +--EXPECT-- +Foo +Bar diff --git a/Zend/tests/generics/reification/func_get_type_args.phpt b/Zend/tests/generics/reification/func_get_type_args.phpt new file mode 100644 index 000000000000..3657bea67e1e --- /dev/null +++ b/Zend/tests/generics/reification/func_get_type_args.phpt @@ -0,0 +1,34 @@ +--TEST-- +Reification: func_get_type_args() reflects turbofish, defaults, and inference +--FILE-- +(T $x): array { + return func_get_type_args(); +} + +var_dump(inspect::(new Foo())); // explicit turbofish +var_dump(inspect::(new Foo())); // U falls back to default +var_dump(inspect(new Bar())); // T inferred, U default +?> +--EXPECT-- +array(2) { + ["T"]=> + string(3) "Foo" + ["U"]=> + string(3) "Bar" +} +array(2) { + ["T"]=> + string(3) "Bar" + ["U"]=> + string(3) "Foo" +} +array(2) { + ["T"]=> + string(3) "Bar" + ["U"]=> + string(3) "Foo" +} diff --git a/Zend/tests/generics/reification/generator_preserves_T.phpt b/Zend/tests/generics/reification/generator_preserves_T.phpt new file mode 100644 index 000000000000..2c4d4c3e43d9 --- /dev/null +++ b/Zend/tests/generics/reification/generator_preserves_T.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: a generator's T-table is preserved across yield/suspend and resume +--FILE-- +(): Generator { + yield T::class; + yield T::class; + yield T::class; +} + +$g = gen::(); +foreach ($g as $v) echo $v, "\n"; + +$g = gen::(); +foreach ($g as $v) echo $v, "\n"; +?> +--EXPECT-- +Foo +Foo +Foo +Bar +Bar +Bar diff --git a/Zend/tests/generics/reification/inference_basic.phpt b/Zend/tests/generics/reification/inference_basic.phpt new file mode 100644 index 000000000000..e2d4076226d3 --- /dev/null +++ b/Zend/tests/generics/reification/inference_basic.phpt @@ -0,0 +1,21 @@ +--TEST-- +Reification: T is inferred from an argument whose declared type is exactly T +--FILE-- +(T $x): string { + return T::class; +} + +echo kind(new Foo()), "\n"; +echo kind(new Bar()), "\n"; + +// Turbofish overrides inference. +echo kind::(new Bar()), "\n"; +?> +--EXPECT-- +Foo +Bar +Foo diff --git a/Zend/tests/generics/reification/interface_dispatched_method.phpt b/Zend/tests/generics/reification/interface_dispatched_method.phpt new file mode 100644 index 000000000000..3fc6ce4efa46 --- /dev/null +++ b/Zend/tests/generics/reification/interface_dispatched_method.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: a generic method called via an interface-typed variable still resolves T +--FILE-- +(): U; +} + +class Factory implements Maker { + public function build(): U { + return new U(); + } +} + +class Foo {} +class Bar {} + +function callIt(Maker $m, string $which): object { + return $which === "foo" ? $m->build::() : $m->build::(); +} + +var_dump(get_class(callIt(new Factory(), "foo"))); +var_dump(get_class(callIt(new Factory(), "bar"))); +?> +--EXPECT-- +string(3) "Foo" +string(3) "Bar" diff --git a/Zend/tests/generics/reification/method_level_T.phpt b/Zend/tests/generics/reification/method_level_T.phpt new file mode 100644 index 000000000000..49c15ffbdffe --- /dev/null +++ b/Zend/tests/generics/reification/method_level_T.phpt @@ -0,0 +1,20 @@ +--TEST-- +Reification: a generic method on a non-generic class can use T inside its body +--FILE-- +(): T { + return new T(); + } +} + +$f = new Factory(); +var_dump($f->build::()->tag); +var_dump($f->build::()->tag); +?> +--EXPECT-- +string(3) "foo" +string(3) "bar" diff --git a/Zend/tests/generics/reification/multiple_inference_targets_agree.phpt b/Zend/tests/generics/reification/multiple_inference_targets_agree.phpt new file mode 100644 index 000000000000..a41442ee15b4 --- /dev/null +++ b/Zend/tests/generics/reification/multiple_inference_targets_agree.phpt @@ -0,0 +1,24 @@ +--TEST-- +Reification: T appearing in multiple arg positions — the first hit wins (subsequent objects of compatible types don't update) +--FILE-- +(T $a, T $b): string { + return T::class; +} + +// Both args are Foo — T inferred as Foo +var_dump(pair(new Foo(), new Foo())); + +// First arg pins T to Bar; second is also a Bar, fine +var_dump(pair(new Bar(), new Bar())); + +// First arg Foo, second Bar — T pinned to Foo (first wins) +var_dump(pair(new Foo(), new Bar())); +?> +--EXPECT-- +string(3) "Foo" +string(3) "Bar" +string(3) "Foo" diff --git a/Zend/tests/generics/reification/recursion_per_frame_table.phpt b/Zend/tests/generics/reification/recursion_per_frame_table.phpt new file mode 100644 index 000000000000..9bddb9205798 --- /dev/null +++ b/Zend/tests/generics/reification/recursion_per_frame_table.phpt @@ -0,0 +1,43 @@ +--TEST-- +Reification: each call frame has its own T-table; recursion across different bindings does not leak +--FILE-- +(int $depth): array { + if ($depth === 0) { + return ["T" => T::class, "U" => U::class]; + } + // Recurse with T and U swapped; on return our slot must still read A/B. + $inner = pair::($depth - 1); + return [ + "outer_T" => T::class, + "outer_U" => U::class, + "inner" => $inner, + ]; +} + +var_dump(pair::(2)); +?> +--EXPECT-- +array(3) { + ["outer_T"]=> + string(1) "A" + ["outer_U"]=> + string(1) "B" + ["inner"]=> + array(3) { + ["outer_T"]=> + string(1) "B" + ["outer_U"]=> + string(1) "A" + ["inner"]=> + array(2) { + ["T"]=> + string(1) "A" + ["U"]=> + string(1) "B" + } + } +} diff --git a/Zend/tests/generics/reification/turbofish_chains_through_outer.phpt b/Zend/tests/generics/reification/turbofish_chains_through_outer.phpt new file mode 100644 index 000000000000..ebb7567f28a0 --- /dev/null +++ b/Zend/tests/generics/reification/turbofish_chains_through_outer.phpt @@ -0,0 +1,21 @@ +--TEST-- +Reification: turbofish whose arg is an outer T resolves against the caller's T-table +--FILE-- +(): string { + return U::class; +} + +function outer(): string { + // The turbofish :: here doesn't have a literal class name; it names + // the enclosing parameter T. The runtime must look T up in the caller + // frame's T-table and pass that to inner. + return inner::(); +} + +echo outer::(), "\n"; +?> +--EXPECT-- +Foo diff --git a/Zend/tests/generics/reification/turbofish_new_T.phpt b/Zend/tests/generics/reification/turbofish_new_T.phpt new file mode 100644 index 000000000000..b70952227b79 --- /dev/null +++ b/Zend/tests/generics/reification/turbofish_new_T.phpt @@ -0,0 +1,17 @@ +--TEST-- +Reification: explicit turbofish populates the T-table; `new T()` in body resolves to the supplied class +--FILE-- +(): T { + return new T(); +} + +var_dump(make::()->kind); +var_dump(make::()->kind); +?> +--EXPECT-- +string(3) "foo" +string(3) "bar" diff --git a/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt b/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt index 09c19fc79aea..fcf1302bddf8 100644 --- a/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt +++ b/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt @@ -2,10 +2,16 @@ Scoping: a file-scope class does NOT shadow a generic type parameter --FILE-- (): void { - new T(); +class T { public int $tag = 42; } +function f(): T { + // Inside f, the bare name T refers to the generic parameter, not the + // file-scope class T. With no binding supplied, the resolver falls back + // to the parameter's bound (the file-scope class via \T) — proving the + // parameter name shadowed the class name in the body. + return new T(); } +$obj = f(); +var_dump($obj->tag); ?> ---EXPECTF-- -Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d +--EXPECT-- +int(42) diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 2dceac2512db..71d8cc7980fc 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -371,6 +371,47 @@ ZEND_FUNCTION(func_get_args) } /* }}} */ +/* {{{ Get an array mapping generic type-parameter names to their resolved + * class names for the currently-executed generic function call. Unbound slots + * (parameter has no turbofish, no default, and no inferable position) yield + * NULL — the caller can detect them and fall back to the parameter's bound. */ +ZEND_FUNCTION(func_get_type_args) +{ + zend_execute_data *ex = EX(prev_execute_data); + + ZEND_PARSE_PARAMETERS_NONE(); + + if (ex && (ZEND_CALL_INFO(ex) & ZEND_CALL_CODE)) { + zend_throw_error(NULL, "func_get_type_args() cannot be called from the global scope"); + RETURN_THROWS(); + } + + if (zend_forbid_dynamic_call() == FAILURE) { + RETURN_THROWS(); + } + + const zend_generic_parameter_list *params = NULL; + if (ex && ZEND_USER_CODE(ex->func->type)) { + params = ex->func->op_array.generic_parameters; + } + if (!params || params->count == 0) { + RETURN_EMPTY_ARRAY(); + } + + zend_type_arg_table *table = ex->type_args; + array_init_size(return_value, params->count); + for (uint32_t i = 0; i < params->count; i++) { + zval entry; + if (table && i < table->count && table->names[i]) { + ZVAL_STR_COPY(&entry, table->names[i]); + } else { + ZVAL_NULL(&entry); + } + zend_hash_add(Z_ARRVAL_P(return_value), params->parameters[i].name, &entry); + } +} +/* }}} */ + /* {{{ Get string length Warning: This function is special-cased by zend_compile.c and so is usually bypassed */ ZEND_FUNCTION(strlen) diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php index 1d405587145d..1c587f9193fe 100644 --- a/Zend/zend_builtin_functions.stub.php +++ b/Zend/zend_builtin_functions.stub.php @@ -30,6 +30,12 @@ function func_get_arg(int $position): mixed {} */ function func_get_args(): array {} +/** + * @return array + * @forbid-dynamic-calls + */ +function func_get_type_args(): array {} + function strlen(string $string): int {} /** @compile-time-eval */ diff --git a/Zend/zend_builtin_functions_arginfo.h b/Zend/zend_builtin_functions_arginfo.h index b3af43fef340..d4aab19bbb82 100644 --- a/Zend/zend_builtin_functions_arginfo.h +++ b/Zend/zend_builtin_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit zend_builtin_functions.stub.php instead. - * Stub hash: 64c61862de86d9968930893bf21b516119724064 */ + * Stub hash: b435953eed1678fa83dd72073c0e95e997ce1d07 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clone, 0, 1, IS_OBJECT, 0) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) @@ -25,6 +25,8 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_func_get_args, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() +#define arginfo_func_get_type_args arginfo_func_get_args + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_strlen, 0, 1, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -254,6 +256,7 @@ ZEND_FUNCTION(zend_version); ZEND_FUNCTION(func_num_args); ZEND_FUNCTION(func_get_arg); ZEND_FUNCTION(func_get_args); +ZEND_FUNCTION(func_get_type_args); ZEND_FUNCTION(strlen); ZEND_FUNCTION(strcmp); ZEND_FUNCTION(strncmp); @@ -330,6 +333,11 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY("func_get_args", zif_func_get_args, arginfo_func_get_args, ZEND_FENTRY_FLAGS(0, ZEND_ACC2_FORBID_DYN_CALLS), NULL, NULL) #elif (PHP_VERSION_ID >= 80400) ZEND_RAW_FENTRY("func_get_args", zif_func_get_args, arginfo_func_get_args, 0, NULL, NULL) +#endif +#if (PHP_VERSION_ID >= 80600) + ZEND_RAW_FENTRY("func_get_type_args", zif_func_get_type_args, arginfo_func_get_type_args, ZEND_FENTRY_FLAGS(0, ZEND_ACC2_FORBID_DYN_CALLS), NULL, NULL) +#elif (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY("func_get_type_args", zif_func_get_type_args, arginfo_func_get_type_args, 0, NULL, NULL) #endif ZEND_FE(strlen, arginfo_strlen) ZEND_RAW_FENTRY("strcmp", zif_strcmp, arginfo_strcmp, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 3c7f1d7404b0..ff312155f60c 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -637,6 +637,95 @@ static void zend_check_generic_argument_bounds( } } +/* Slots left NULL mean "fall back to the parameter's bound". Order of resolution + * for each slot: explicit turbofish arg → parameter's declared default → + * value-directed inference from any argument whose pre-erasure type is a + * direct TYPE_PARAMETER reference to this slot. */ +ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( + zend_execute_data *call, const zend_type *args_box) +{ + const zend_function *fbc = call->func; + const zend_generic_parameter_list *params = NULL; + const zend_op_array *op_array = NULL; + if (ZEND_USER_CODE(fbc->common.type)) { + params = fbc->op_array.generic_parameters; + op_array = &fbc->op_array; + } + if (!params || params->count == 0) { + return NULL; + } + + zend_type_arg_table *table = zend_type_arg_table_alloc(params->count); + uint32_t passed = 0; + const zend_type *passed_args = NULL; + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + passed = nwa->count; + passed_args = nwa->args; + } + + zend_execute_data *caller = EG(current_execute_data); + for (uint32_t i = 0; i < params->count; i++) { + zend_type src = ZEND_TYPE_INIT_NONE(0); + if (i < passed) { + src = passed_args[i]; + } else if (ZEND_TYPE_IS_SET(params->parameters[i].default_type)) { + src = params->parameters[i].default_type; + } + if (!ZEND_TYPE_IS_SET(src)) { + continue; + } + /* A turbofish arg can itself name an outer generic parameter (e.g. + * `inner::(...)` inside `function outer`). Resolve those + * against the caller frame's T-table so the binding chains. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(src)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(src); + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE + && caller && caller->type_args + && ref->index < caller->type_args->count + && caller->type_args->names[ref->index]) { + table->names[i] = zend_string_copy(caller->type_args->names[ref->index]); + } + continue; + } + table->names[i] = zend_type_arg_canonical_name(src); + } + + /* Inference for unset slots. Composite shapes (array, Box, ?T) are + * skipped — only bare top-level T at a value-parameter position. The + * inferable_mask short-circuits when no inferable slot remains unset. */ + uint64_t unbound_inferable = params->inferable_mask; + for (uint32_t i = 0; i < params->count && i < 64; i++) { + if (table->names[i]) { + unbound_inferable &= ~((uint64_t) 1 << i); + } + } + if (unbound_inferable + && op_array + && op_array->generic_types + && op_array->generic_types->parameters) { + HashTable *pre = op_array->generic_types->parameters; + uint32_t num_args = ZEND_CALL_NUM_ARGS(call); + zend_ulong arg_idx; + zend_type *pe_type_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + if (arg_idx >= num_args) continue; + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + if (ZEND_TYPE_FULL_MASK(*pe_type_ptr) & MAY_BE_NULL) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= table->count) continue; + if (table->names[ref->index]) continue; + zval *arg = ZEND_CALL_ARG(call, (uint32_t) arg_idx + 1); + if (Z_TYPE_P(arg) == IS_OBJECT) { + table->names[ref->index] = zend_string_copy(Z_OBJCE_P(arg)->name); + } + } ZEND_HASH_FOREACH_END(); + } + + return table; +} + ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box) { const zend_generic_parameter_list *params = NULL; @@ -658,16 +747,28 @@ ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32 total); return; } - if (arity < required) { - zend_throw_error(zend_ce_argument_count_error, - "Too few generic type arguments to %s%s%s(), %u passed and %s %u expected", - fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", - fbc->common.scope ? "::" : "", - fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", - arity, - required == total ? "exactly" : "at least", - required); - return; + if (arity > 0 && arity < required) { + /* A missing slot is treated as supplied for arity purposes if a + * value-parameter can pin it via inference. The compile-time + * inferable_mask precomputes that set. */ + uint64_t inferable = params ? params->inferable_mask : 0; + uint32_t still_required = required; + for (uint32_t i = arity; i < required; i++) { + if (inferable & ((uint64_t) 1 << i)) { + still_required--; + } + } + if (arity < still_required) { + zend_throw_error(zend_ce_argument_count_error, + "Too few generic type arguments to %s%s%s(), %u passed and %s %u expected", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + arity, + required == total ? "exactly" : "at least", + required); + return; + } } if (args_box) { @@ -724,19 +825,33 @@ static zend_type zend_compile_turbofish_args_type(zend_ast *turbofish_ast) return result; } -static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result) +static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc) { - if (turbofish_ast == NULL) { - return; - } - uint32_t arity = zend_ast_get_list(turbofish_ast)->children; - ZEND_ASSERT(arity > 0 && arity <= ZEND_GENERIC_MAX_PARAMS); + uint32_t arity = 0; + uint32_t args_id = 0; + + if (turbofish_ast) { + arity = zend_ast_get_list(turbofish_ast)->children; + ZEND_ASSERT(arity > 0 && arity <= ZEND_GENERIC_MAX_PARAMS); - zend_type args_type = zend_compile_turbofish_args_type(turbofish_ast); - zend_generic_type_table *table = - zend_generic_get_or_create_op_array_table(CG(active_op_array)); - uint32_t args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; - zend_generic_type_table_set_turbofish_args(table, args_id, args_type); + zend_type args_type = zend_compile_turbofish_args_type(turbofish_ast); + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, args_type); + } else { + /* No turbofish: only call sites get a defaults/inference-driven VERIFY + * (NEW already emitted it above the args). Skip when fbc is known and + * statically non-generic; emit speculatively otherwise — the runtime + * handler short-circuits when the resolved callee is non-generic. */ + if (kind != ZEND_VERIFY_ARITY_KIND_CALL) { + return; + } + if (fbc && (!ZEND_USER_CODE(fbc->common.type) + || !fbc->op_array.generic_parameters)) { + return; + } + } zend_op *opline = get_next_op(); opline->opcode = ZEND_VERIFY_GENERIC_ARGUMENTS; @@ -1872,12 +1987,19 @@ static zend_string *zend_resolve_class_name_ast(zend_ast *ast) /* {{{ */ zend_error_noreturn(E_COMPILE_ERROR, "Illegal class name"); } - if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ - && zend_generic_lookup(Z_STR_P(class_name))) { - zend_error_noreturn(E_COMPILE_ERROR, - "Cannot use generic type parameter %s as a class reference at runtime; " - "bound-erased generic types have no runtime representation", - Z_STRVAL_P(class_name)); + /* Class-level generic params remain bound-erased and have no runtime + * representation; function-level params are routed to a TYPE_PARAM fetch + * by zend_compile_class_ref before reaching here. */ + if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + zend_generic_origin origin; + zend_generic_parameter *param = zend_generic_lookup_full( + Z_STR_P(class_name), &origin, NULL); + if (param && origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot use class-level generic type parameter %s as a class reference at runtime; " + "bound-erased generic types have no runtime representation", + Z_STRVAL_P(class_name)); + } } return zend_resolve_class_name(Z_STR_P(class_name), ast->attr); @@ -2707,6 +2829,16 @@ static bool zend_try_compile_const_expr_resolve_class_name(zval *zv, zend_ast *c zend_error_noreturn(E_COMPILE_ERROR, "Illegal class name"); } + /* Function-level T::class resolves at runtime through the frame's T-table. */ + if ((class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + zend_generic_origin origin; + zend_generic_parameter *param = zend_generic_lookup_full( + Z_STR_P(class_name), &origin, NULL); + if (param && origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + return false; + } + } + fetch_type = zend_get_class_fetch_type(Z_STR_P(class_name)); zend_ensure_valid_class_fetch_type(fetch_type); @@ -3829,6 +3961,21 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f return; } + /* Function/method-level T in expression position (new T(), T::FOO, + * instanceof T, T::method()) resolves through the call frame's T-table + * at runtime. */ + if ((name_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + zend_generic_origin origin; + uint32_t param_index = 0; + zend_generic_parameter *param = zend_generic_lookup_full( + zend_ast_get_str(name_ast), &origin, ¶m_index); + if (param && origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + result->op_type = IS_UNUSED; + result->u.op.num = zend_pack_type_param_fetch(param_index, fetch_flags); + return; + } + } + fetch_type = zend_get_class_fetch_type(zend_ast_get_str(name_ast)); if (ZEND_FETCH_CLASS_DEFAULT == fetch_type) { result->op_type = IS_CONST; @@ -4943,7 +5090,12 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze zend_op *opline; uint32_t opnum_init = get_next_op_number() - 1; - zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result); + /* NEW emits VERIFY before args so the constructor RECVs see the + * monomorph's substituted arg_info; CALL emits it after args so inference + * from arg values can fill type-parameter slots. */ + if (verify_kind == ZEND_VERIFY_ARITY_KIND_NEW) { + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc); + } if (args_ast->kind == ZEND_AST_CALLABLE_CONVERT) { opline = &CG(active_op_array)->opcodes[opnum_init]; @@ -4978,6 +5130,10 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze bool may_have_extra_named_args; uint32_t arg_count = zend_compile_args(args_ast, fbc, &may_have_extra_named_args); + if (verify_kind == ZEND_VERIFY_ARITY_KIND_CALL) { + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc); + } + zend_do_extended_fcall_begin(); opline = &CG(active_op_array)->opcodes[opnum_init]; @@ -8115,6 +8271,20 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ zend_error_noreturn(E_COMPILE_ERROR, "Bad class name in the catch statement"); } + /* catch needs a class to check at throw time; we don't currently + * resolve generic type parameters through the catch path. */ + if (class_ast->kind == ZEND_AST_ZVAL) { + zend_ast *bare = class_ast; + if (bare->kind == ZEND_AST_GENERIC_NAMED_TYPE) bare = bare->child[0]; + if ((bare->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ + && zend_generic_lookup(zend_ast_get_str(bare))) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot use generic type parameter %s as a class reference at runtime; " + "bound-erased generic types have no runtime representation", + ZSTR_VAL(zend_ast_get_str(bare))); + } + } + opnum_catch = get_next_op_number(); if (i == 0 && j == 0) { CG(active_op_array)->try_catch_array[try_catch_offset].catch_op = opnum_catch; @@ -9306,6 +9476,18 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 } zend_generic_type_table_set_parameter( zend_generic_get_or_create_op_array_table(op_array), i, pre); + /* Track which generic params can be inferred from a value + * argument: only bare, non-nullable T at the top level. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(pre) + && !(ZEND_TYPE_FULL_MASK(pre) & MAY_BE_NULL)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(pre); + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE + && ref->index < 64 + && op_array->generic_parameters) { + op_array->generic_parameters->inferable_mask |= + (uint64_t) 1 << ref->index; + } + } } if (forced_allow_nullable) { zend_string *func_name = get_function_or_method_name((zend_function *) op_array); @@ -12683,7 +12865,18 @@ static void zend_compile_class_name(znode *result, const zend_ast *ast) /* {{{ * if (class_ast->kind == ZEND_AST_ZVAL) { zend_op *opline = zend_emit_op_tmp(result, ZEND_FETCH_CLASS_NAME, NULL, NULL); - opline->op1.num = zend_get_class_fetch_type(zend_ast_get_str(class_ast)); + uint32_t op1_num = zend_get_class_fetch_type(zend_ast_get_str(class_ast)); + if (op1_num == ZEND_FETCH_CLASS_DEFAULT + && (class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + zend_generic_origin origin; + uint32_t param_index = 0; + zend_generic_parameter *param = zend_generic_lookup_full( + zend_ast_get_str(class_ast), &origin, ¶m_index); + if (param && origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + op1_num = zend_pack_type_param_fetch(param_index, 0); + } + } + opline->op1.num = op1_num; } else { znode expr_node; zend_compile_expr(&expr_node, class_ast); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 6be418cbf0d4..526f0987c114 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -148,6 +148,7 @@ typedef struct _zend_generic_parameter { typedef struct _zend_generic_parameter_list { uint32_t count; + uint64_t inferable_mask; /* bit i set when some value-parameter's pre-erasure type is exactly param i */ zend_generic_parameter parameters[1]; } zend_generic_parameter_list; @@ -197,6 +198,24 @@ ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32 ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint32_t arity, const zend_type *args_box); ZEND_API const zend_type *zend_generic_get_turbofish_args(const zend_op_array *caller_op_array, uint32_t args_id); +/* Per-call-frame mapping of generic type parameters to bound class names. NULL + * entries mean "use the parameter's bound" (the erased default). Lifetime is + * tied to the call frame: allocated by VERIFY_GENERIC_ARGUMENTS for a function + * call (or inferred at RECV time), freed when the frame unwinds. */ +typedef struct _zend_type_arg_table { + uint32_t count; + zend_string *names[1]; +} zend_type_arg_table; + +#define ZEND_TYPE_ARG_TABLE_SIZE(count) \ + (sizeof(zend_type_arg_table) + ((count) - 1) * sizeof(zend_string *)) + +ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count); +ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); +ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); +ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); +ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type); + typedef union _zend_parser_stack_elem { zend_ast *ast; zend_string *str; @@ -740,6 +759,7 @@ struct _zend_execute_data { zend_array *symbol_table; void **run_time_cache; /* cache op_array->run_time_cache */ zend_array *extra_named_params; + zend_type_arg_table *type_args; /* generic type-argument table; NULL when none */ }; #define ZEND_CALL_HAS_THIS IS_OBJECT_EX @@ -1143,7 +1163,18 @@ ZEND_API bool zend_type_contains_type_parameter(zend_type type); #define ZEND_FETCH_CLASS_AUTO 4 #define ZEND_FETCH_CLASS_INTERFACE 5 #define ZEND_FETCH_CLASS_TRAIT 6 +#define ZEND_FETCH_CLASS_TYPE_PARAM 7 #define ZEND_FETCH_CLASS_MASK 0x0f +#define ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT 16 + +static zend_always_inline uint32_t zend_pack_type_param_fetch(uint32_t param_index, uint32_t flags) { + ZEND_ASSERT(param_index < (1u << (32 - ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT))); + return ZEND_FETCH_CLASS_TYPE_PARAM | (param_index << ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT) | flags; +} + +static zend_always_inline uint32_t zend_unpack_type_param_index(uint32_t fetch_type) { + return fetch_type >> ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT; +} #define ZEND_FETCH_CLASS_NO_AUTOLOAD 0x80 #define ZEND_FETCH_CLASS_SILENT 0x0100 #define ZEND_FETCH_CLASS_EXCEPTION 0x0200 diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 1b28ce25fe37..d7edf3372c31 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4302,6 +4302,10 @@ static zend_always_inline void i_free_compiled_variables(zend_execute_data *exec cv++; count--; } + if (UNEXPECTED(execute_data->type_args)) { + zend_type_arg_table_destroy(execute_data->type_args); + execute_data->type_args = NULL; + } } /* }}} */ diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index ba48b19bcfe1..c822695a7750 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -338,6 +338,7 @@ static zend_always_inline void zend_vm_init_call_frame(zend_execute_data *call, Z_PTR(call->This) = object_or_called_scope; ZEND_CALL_INFO(call) = call_info; ZEND_CALL_NUM_ARGS(call) = num_args; + call->type_args = NULL; } static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, uint32_t call_info, zend_function *func, uint32_t num_args, void *object_or_called_scope) @@ -411,6 +412,11 @@ static zend_always_inline void zend_vm_stack_free_call_frame_ex(uint32_t call_in { ZEND_ASSERT_VM_STACK_GLOBAL; + if (UNEXPECTED(call->type_args != NULL)) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } + if (UNEXPECTED(call_info & ZEND_CALL_ALLOCATED)) { zend_vm_stack p = EG(vm_stack); zend_vm_stack prev = p->prev; diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index e99e4e3e7e8e..a6b830357896 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1729,6 +1729,38 @@ static ZEND_COLD void report_class_fetch_error(const zend_string *class_name, ui } } +/* Resolve a function/method-level generic type parameter at runtime by reading + * the current frame's T-table. Falls back to the parameter's declared bound when + * no concrete binding was supplied (e.g. the function was called without + * turbofish and inference produced nothing). Throws when neither a binding nor + * a usable class bound is available. */ +ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type) +{ + zend_execute_data *ex = EG(current_execute_data); + zend_type_arg_table *table = ex ? ex->type_args : NULL; + if (table && param_index < table->count && table->names[param_index]) { + return zend_fetch_class_by_name(table->names[param_index], NULL, fetch_type); + } + + if (ex && ZEND_USER_CODE(ex->func->type)) { + zend_generic_parameter_list *params = ex->func->op_array.generic_parameters; + if (params && param_index < params->count) { + zend_type bound = params->parameters[param_index].bound; + if (ZEND_TYPE_HAS_NAME(bound)) { + return zend_fetch_class_by_name(ZEND_TYPE_NAME(bound), NULL, fetch_type); + } + ZEND_ASSERT(params->parameters[param_index].name); + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve generic type parameter %s at runtime: no binding was supplied and its bound is not a class", + ZSTR_VAL(params->parameters[param_index].name)); + return NULL; + } + } + + zend_throw_or_error(fetch_type, NULL, "Cannot resolve generic type parameter at runtime: not in a generic function call"); + return NULL; +} + zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type) /* {{{ */ { zend_class_entry *ce, *scope; @@ -1736,6 +1768,9 @@ zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type) check_fetch_type: switch (fetch_sub_type) { + case ZEND_FETCH_CLASS_TYPE_PARAM: + return zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); case ZEND_FETCH_CLASS_SELF: scope = zend_get_executed_scope(); if (UNEXPECTED(!scope)) { diff --git a/Zend/zend_generators.c b/Zend/zend_generators.c index 52d409f2e69b..50504be4ea36 100644 --- a/Zend/zend_generators.c +++ b/Zend/zend_generators.c @@ -46,6 +46,7 @@ ZEND_API void zend_generator_restore_call_stack(zend_generator *generator) /* {{ Z_PTR(call->This)); memcpy(((zval*)new_call) + ZEND_CALL_FRAME_SLOT, ((zval*)call) + ZEND_CALL_FRAME_SLOT, ZEND_CALL_NUM_ARGS(call) * sizeof(zval)); new_call->extra_named_params = call->extra_named_params; + new_call->type_args = call->type_args; new_call->prev_execute_data = prev_call; prev_call = new_call; @@ -84,6 +85,10 @@ ZEND_API zend_execute_data* zend_generator_freeze_call_stack(zend_execute_data * new_call->prev_execute_data = prev_call; prev_call = new_call; + /* Frozen copy owns type_args; null the original so frame teardown + * below does not free what the generator still needs on resume. */ + call->type_args = NULL; + new_call = call->prev_execute_data; zend_vm_stack_free_call_frame(call); call = new_call; @@ -146,6 +151,10 @@ ZEND_API void zend_generator_close(zend_generator *generator, bool finished_exec if (EX_CALL_INFO() & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) { zend_free_extra_named_params(execute_data->extra_named_params); } + if (execute_data->type_args) { + zend_type_arg_table_destroy(execute_data->type_args); + execute_data->type_args = NULL; + } if (EX_CALL_INFO() & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(execute_data->This)); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index f5c8f11b3078..2dc4abd9a05b 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -145,6 +145,7 @@ ZEND_API zend_generic_parameter_list *zend_generic_parameter_list_alloc(uint32_t ZEND_ASSERT(count > 0); zend_generic_parameter_list *list = pemalloc(ZEND_GENERIC_PARAMETER_LIST_SIZE(count), persistent); list->count = count; + list->inferable_mask = 0; for (uint32_t i = 0; i < count; i++) { list->parameters[i].name = NULL; list->parameters[i].variance = 0; @@ -290,6 +291,36 @@ ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->turbofish_args), op_num, zend_type_box(type)); } +ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count) { + zend_type_arg_table *table = emalloc(ZEND_TYPE_ARG_TABLE_SIZE(count)); + table->count = count; + for (uint32_t i = 0; i < count; i++) { + table->names[i] = NULL; + } + return table; +} + +ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table) { + if (!table) { + return; + } + for (uint32_t i = 0; i < table->count; i++) { + if (table->names[i]) { + zend_string_release(table->names[i]); + } + } + efree(table); +} + +/* Returns NULL when the type is not a concrete class reference — the resolver + * reads NULL as "fall back to the parameter's bound". */ +ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type) { + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type) || ZEND_TYPE_HAS_NAME(type)) { + return zend_type_to_canonical_string(type); + } + return NULL; +} + ZEND_API void zend_free_internal_arg_info(zend_internal_function *function, bool persistent) { if (function->arg_info) { diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index d12f17da2094..d2897d349113 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -8989,7 +8989,26 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) SAVE_OPLINE(); if (OP1_TYPE == IS_UNUSED) { + /* Speculative emission for dispatchable calls: when there's no + * turbofish AND the resolved callee turns out to be non-generic, + * there's nothing to verify and no table to build. With turbofish + * present the arity check still needs to fire (the user supplied + * type args to a non-generic callee — explicit "too many" error). */ + if (args_box == NULL + && (!ZEND_USER_CODE(call->func->type) + || !call->func->op_array.generic_parameters)) { + ZEND_VM_NEXT_OPCODE(); + } zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + } } else { zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); @@ -9094,6 +9113,17 @@ ZEND_VM_HANDLER(157, ZEND_FETCH_CLASS_NAME, CV|TMP|UNUSED|CLASS_FETCH, ANY) } fetch_type = opline->op1.num; + if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index c941c4ddd4ab..b5cd5b286058 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -18306,6 +18306,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; + if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -22052,7 +22063,26 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { + /* Speculative emission for dispatchable calls: when there's no + * turbofish AND the resolved callee turns out to be non-generic, + * there's nothing to verify and no table to build. With turbofish + * present the arity check still needs to fire (the user supplied + * type args to a non-generic callee — explicit "too many" error). */ + if (args_box == NULL + && (!ZEND_USER_CODE(call->func->type) + || !call->func->op_array.generic_parameters)) { + ZEND_VM_NEXT_OPCODE(); + } zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + } } else { zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); @@ -32991,6 +33021,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; + if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -37485,7 +37526,26 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { + /* Speculative emission for dispatchable calls: when there's no + * turbofish AND the resolved callee turns out to be non-generic, + * there's nothing to verify and no table to build. With turbofish + * present the arity check still needs to fire (the user supplied + * type args to a non-generic callee — explicit "too many" error). */ + if (args_box == NULL + && (!ZEND_USER_CODE(call->func->type) + || !call->func->op_array.generic_parameters)) { + ZEND_VM_NEXT_OPCODE(); + } zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + } } else { zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); @@ -41078,6 +41138,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; + if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -71109,6 +71180,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; + if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -74755,7 +74837,26 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { + /* Speculative emission for dispatchable calls: when there's no + * turbofish AND the resolved callee turns out to be non-generic, + * there's nothing to verify and no table to build. With turbofish + * present the arity check still needs to fire (the user supplied + * type args to a non-generic callee — explicit "too many" error). */ + if (args_box == NULL + && (!ZEND_USER_CODE(call->func->type) + || !call->func->op_array.generic_parameters)) { + ZEND_VM_NEXT_OPCODE(); + } zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + } } else { zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); @@ -85694,6 +85795,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; + if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -90188,7 +90300,26 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { + /* Speculative emission for dispatchable calls: when there's no + * turbofish AND the resolved callee turns out to be non-generic, + * there's nothing to verify and no table to build. With turbofish + * present the arity check still needs to fire (the user supplied + * type args to a non-generic callee — explicit "too many" error). */ + if (args_box == NULL + && (!ZEND_USER_CODE(call->func->type) + || !call->func->op_array.generic_parameters)) { + ZEND_VM_NEXT_OPCODE(); + } zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + } } else { zval *new_obj = EX_VAR(opline->op1.var); zend_class_entry *ce = Z_OBJCE_P(new_obj); @@ -93781,6 +93912,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; + if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); From 9e1631d2840a91619c85e2438549a5b663aff677 Mon Sep 17 00:00:00 2001 From: Rob Landers Date: Tue, 19 May 2026 19:05:57 +0200 Subject: [PATCH 05/30] more reification --- .../declaration/variance/rfc_example.phpt | 2 +- .../erasure/instanceof_args_discarded.phpt | 6 +- .../generics/errors/type_param_in_catch.phpt | 15 +- .../generics/reification/catch_with_args.phpt | 27 + .../reification/class_level_T_in_catch.phpt | 29 + .../class_level_T_in_method_body.phpt | 56 + .../reification/class_level_T_inherited.phpt | 21 + .../reification/func_get_type_args.phpt | 6 +- .../function_level_T_in_catch.phpt | 26 + .../generics/reification/inference_basic.phpt | 11 +- .../instanceof_distinguishes_monos.phpt | 25 + .../reification/instanceof_variance.phpt | 46 + .../generics/syntax/catch_with_args.phpt | 17 +- .../generics/syntax/instanceof_with_args.phpt | 8 +- .../turbofish/instanceof_with_args.phpt | 19 +- Zend/zend.h | 1 + Zend/zend_compile.c | 192 +- Zend/zend_compile.h | 14 +- Zend/zend_execute_API.c | 61 +- Zend/zend_inheritance.c | 7 + Zend/zend_opcode.c | 10 + Zend/zend_operators.c | 68 +- Zend/zend_vm_def.h | 27 +- Zend/zend_vm_execute.h | 575 ++++-- Zend/zend_vm_handlers.h | 1615 +++++++++-------- 25 files changed, 1776 insertions(+), 1108 deletions(-) create mode 100644 Zend/tests/generics/reification/catch_with_args.phpt create mode 100644 Zend/tests/generics/reification/class_level_T_in_catch.phpt create mode 100644 Zend/tests/generics/reification/class_level_T_in_method_body.phpt create mode 100644 Zend/tests/generics/reification/class_level_T_inherited.phpt create mode 100644 Zend/tests/generics/reification/function_level_T_in_catch.phpt create mode 100644 Zend/tests/generics/reification/instanceof_distinguishes_monos.phpt create mode 100644 Zend/tests/generics/reification/instanceof_variance.phpt diff --git a/Zend/tests/generics/declaration/variance/rfc_example.phpt b/Zend/tests/generics/declaration/variance/rfc_example.phpt index b34e65f25a6e..e65c030b9369 100644 --- a/Zend/tests/generics/declaration/variance/rfc_example.phpt +++ b/Zend/tests/generics/declaration/variance/rfc_example.phpt @@ -35,7 +35,7 @@ function identity(T $value): T { $greeting = new Box::("hello, world"); $paired = $greeting->zip::(42); $swapped = $paired->value->swap(); -$result = identity::>($swapped); +$result = identity($swapped); var_dump($result->left); var_dump($result->right); diff --git a/Zend/tests/generics/erasure/instanceof_args_discarded.phpt b/Zend/tests/generics/erasure/instanceof_args_discarded.phpt index 459815492d66..f97188fcae52 100644 --- a/Zend/tests/generics/erasure/instanceof_args_discarded.phpt +++ b/Zend/tests/generics/erasure/instanceof_args_discarded.phpt @@ -1,5 +1,5 @@ --TEST-- -Erasure: instanceof type arguments discarded at runtime +Erasure: instanceof on a non-generic class with type arguments resolves to the canonical name (which does not exist) and returns false --FILE-- ); ?> --EXPECT-- bool(true) -bool(true) -bool(true) +bool(false) +bool(false) diff --git a/Zend/tests/generics/errors/type_param_in_catch.phpt b/Zend/tests/generics/errors/type_param_in_catch.phpt index 9141899cb0f9..7d46d8f88380 100644 --- a/Zend/tests/generics/errors/type_param_in_catch.phpt +++ b/Zend/tests/generics/errors/type_param_in_catch.phpt @@ -1,10 +1,17 @@ --TEST-- -Errors: type parameter cannot be used at runtime in `catch` +catch (T $e) silently does not match when T has no usable binding --FILE-- (): void { - try { /* ... */ } catch (T $e) {} + // T has no class bound and no caller-supplied binding. catch (T $e) + // is therefore "catch nothing"; the thrown exception propagates out. + try { throw new Exception("boom"); } catch (T $e) { echo "caught\n"; } +} +try { + f(); +} catch (Throwable $e) { + echo "outer: ", $e->getMessage(), "\n"; } ?> ---EXPECTF-- -Fatal error: Cannot use generic type parameter T as a class reference at runtime; bound-erased generic types have no runtime representation in %s on line %d +--EXPECT-- +outer: boom diff --git a/Zend/tests/generics/reification/catch_with_args.phpt b/Zend/tests/generics/reification/catch_with_args.phpt new file mode 100644 index 000000000000..aa97c7e02395 --- /dev/null +++ b/Zend/tests/generics/reification/catch_with_args.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: catch with concrete type arguments matches the corresponding monomorph +--FILE-- + extends Exception {} + +// Throw a synthesized monomorph of Bag and catch it via the canonical name. +try { + throw new Bag::("payload"); +} catch (Bag $e) { + echo "caught Bag: ", $e->getMessage(), "\n"; +} + +// Wrong arg in the catch — the canonical names differ, so no match. +try { + try { + throw new Bag::("payload"); + } catch (Bag $e) { + echo "inner caught\n"; + } +} catch (Bag $e) { + echo "outer caught Bag (parent): ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +caught Bag: payload +outer caught Bag (parent): payload diff --git a/Zend/tests/generics/reification/class_level_T_in_catch.phpt b/Zend/tests/generics/reification/class_level_T_in_catch.phpt new file mode 100644 index 000000000000..d0f7c5513f15 --- /dev/null +++ b/Zend/tests/generics/reification/class_level_T_in_catch.phpt @@ -0,0 +1,29 @@ +--TEST-- +Reification: catch (T $e) inside a method body matches against the class-level T binding +--FILE-- + { + public function trap(callable $fn): string { + try { + $fn(); + return "no-throw"; + } catch (T $e) { + return "caught " . $e::class . ": " . $e->getMessage(); + } + } +} + +$h = new Handler::(); +echo $h->trap(fn() => throw new MyExc("hi")), "\n"; // T = MyExc → caught +try { + $h->trap(fn() => throw new OtherExc("bye")); // T = MyExc → rethrows +} catch (Throwable $e) { + echo "outer ", $e::class, ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +caught MyExc: hi +outer OtherExc: bye diff --git a/Zend/tests/generics/reification/class_level_T_in_method_body.phpt b/Zend/tests/generics/reification/class_level_T_in_method_body.phpt new file mode 100644 index 000000000000..bba1bc1d0f47 --- /dev/null +++ b/Zend/tests/generics/reification/class_level_T_in_method_body.phpt @@ -0,0 +1,56 @@ +--TEST-- +Reification: class-level T resolves at runtime inside instance method bodies +--FILE-- + { + public function summary(object $x): array { + return [ + "new T()" => get_class(new T()), + "T::KIND" => T::KIND, + "T::class" => T::class, + "T::make()" => get_class(T::make()), + "instanceof" => $x instanceof T, + ]; + } +} + +var_dump((new Box::())->summary(new Foo())); +var_dump((new Box::())->summary(new Foo())); +?> +--EXPECT-- +array(5) { + ["new T()"]=> + string(3) "Foo" + ["T::KIND"]=> + string(3) "foo" + ["T::class"]=> + string(3) "Foo" + ["T::make()"]=> + string(3) "Foo" + ["instanceof"]=> + bool(true) +} +array(5) { + ["new T()"]=> + string(3) "Bar" + ["T::KIND"]=> + string(3) "bar" + ["T::class"]=> + string(3) "Bar" + ["T::make()"]=> + string(3) "Bar" + ["instanceof"]=> + bool(false) +} diff --git a/Zend/tests/generics/reification/class_level_T_inherited.phpt b/Zend/tests/generics/reification/class_level_T_inherited.phpt new file mode 100644 index 000000000000..0eab27084809 --- /dev/null +++ b/Zend/tests/generics/reification/class_level_T_inherited.phpt @@ -0,0 +1,21 @@ +--TEST-- +Reification: a subclass `extends Box` carries the parent's T binding through inherited method bodies +--FILE-- + { + public function make(): T { + return new T(); + } +} + +class FooBox extends Box {} + +// FooBox::make is inherited from Box; the binding T=Foo on FooBox's parent +// must be visible to the inherited body via the called scope. +$fb = new FooBox(); +var_dump(get_class($fb->make())); +?> +--EXPECT-- +string(3) "Foo" diff --git a/Zend/tests/generics/reification/func_get_type_args.phpt b/Zend/tests/generics/reification/func_get_type_args.phpt index 3657bea67e1e..0730b21c6775 100644 --- a/Zend/tests/generics/reification/func_get_type_args.phpt +++ b/Zend/tests/generics/reification/func_get_type_args.phpt @@ -9,9 +9,9 @@ function inspect(T $x): array { return func_get_type_args(); } -var_dump(inspect::(new Foo())); // explicit turbofish -var_dump(inspect::(new Foo())); // U falls back to default -var_dump(inspect(new Bar())); // T inferred, U default +var_dump(inspect::(new Foo())); // explicit turbofish for T and U +var_dump(inspect::(new Bar())); // U falls back to its default Foo +var_dump(inspect(new Bar())); // T inferred from arg, U default ?> --EXPECT-- array(2) { diff --git a/Zend/tests/generics/reification/function_level_T_in_catch.phpt b/Zend/tests/generics/reification/function_level_T_in_catch.phpt new file mode 100644 index 000000000000..2e9f3a43be55 --- /dev/null +++ b/Zend/tests/generics/reification/function_level_T_in_catch.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: catch (T $e) inside a function body matches against the function-level T binding +--FILE-- +(callable $fn): string { + try { + $fn(); + return "no-throw"; + } catch (T $e) { + return "caught " . $e::class . ": " . $e->getMessage(); + } +} + +echo trap::(fn() => throw new MyExc("a")), "\n"; // matches +try { + trap::(fn() => throw new OtherExc("b")); // doesn't match +} catch (Throwable $e) { + echo "outer ", $e::class, ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +caught MyExc: a +outer OtherExc: b diff --git a/Zend/tests/generics/reification/inference_basic.phpt b/Zend/tests/generics/reification/inference_basic.phpt index e2d4076226d3..240d9ed4da20 100644 --- a/Zend/tests/generics/reification/inference_basic.phpt +++ b/Zend/tests/generics/reification/inference_basic.phpt @@ -1,5 +1,5 @@ --TEST-- -Reification: T is inferred from an argument whose declared type is exactly T +Reification: T is inferred from an argument whose declared type is exactly T; the substituted parameter type is enforced when turbofish disagrees with the value --FILE-- (T $x): string { echo kind(new Foo()), "\n"; echo kind(new Bar()), "\n"; -// Turbofish overrides inference. -echo kind::(new Bar()), "\n"; +try { + kind::(new Bar()); +} catch (TypeError $e) { + echo "TypeError: ", $e->getMessage(), "\n"; +} ?> --EXPECT-- Foo Bar -Foo +TypeError: kind(): Argument #1 ($x) must be of type Foo, Bar given diff --git a/Zend/tests/generics/reification/instanceof_distinguishes_monos.phpt b/Zend/tests/generics/reification/instanceof_distinguishes_monos.phpt new file mode 100644 index 000000000000..9d71095bbc65 --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_distinguishes_monos.phpt @@ -0,0 +1,25 @@ +--TEST-- +Reification: instanceof distinguishes monos by their canonical argument tuple +--FILE-- + {} + +$bd = new Box::(); +$ba = new Box::(); + +// Each mono extends the bare class but is otherwise distinct. +var_dump($bd instanceof Box); // true: parent-name chain +var_dump($bd instanceof Box); // true: same canonical mono +var_dump($bd instanceof Box); // false: invariant distinction +var_dump($ba instanceof Box); // false +var_dump($ba instanceof Box); // true +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(false) +bool(true) diff --git a/Zend/tests/generics/reification/instanceof_variance.phpt b/Zend/tests/generics/reification/instanceof_variance.phpt new file mode 100644 index 000000000000..6be079f63d10 --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_variance.phpt @@ -0,0 +1,46 @@ +--TEST-- +Reification: instanceof respects variance markers on the generic base +--FILE-- + {} // invariant +class Producer<+T : object> {} // covariant +class Consumer<-T : object> {} // contravariant + +$dogBox = new Box::(); +$dogProd = new Producer::(); +$animalCons = new Consumer::(); + +// Invariant: only same-arg passes. +var_dump($dogBox instanceof Box); // true (identical) +var_dump($dogBox instanceof Box); // false +var_dump($dogBox instanceof Box); // false + +// Covariant: Dog <: Animal, so Producer <: Producer. +var_dump($dogProd instanceof Producer); // true +var_dump($dogProd instanceof Producer); // false +var_dump($dogProd instanceof Producer); // true + +// Contravariant: Animal :> Dog, so Consumer <: Consumer. +var_dump($animalCons instanceof Consumer); // true +var_dump($animalCons instanceof Consumer); // true +var_dump($animalCons instanceof Consumer);// true + +// Reverse contravariant fails. +$dogCons = new Consumer::(); +var_dump($dogCons instanceof Consumer); // false +?> +--EXPECT-- +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) diff --git a/Zend/tests/generics/syntax/catch_with_args.phpt b/Zend/tests/generics/syntax/catch_with_args.phpt index bf82150c2660..d97bf24cd8b9 100644 --- a/Zend/tests/generics/syntax/catch_with_args.phpt +++ b/Zend/tests/generics/syntax/catch_with_args.phpt @@ -1,13 +1,20 @@ --TEST-- -Generic syntax: catch with type arguments (args discarded at runtime) +Generic syntax: catch with type arguments compares against the monomorph canonical name --FILE-- does not exist as a +// class. catch (MyErr) therefore never matches; the original exception +// propagates to the outer catch. try { - throw new MyErr('boom'); -} catch (MyErr $e) { - echo "caught: ", $e->getMessage(), "\n"; + try { + throw new MyErr('boom'); + } catch (MyErr $e) { + echo "inner caught: ", $e->getMessage(), "\n"; + } +} catch (MyErr $e) { + echo "outer caught: ", $e->getMessage(), "\n"; } ?> --EXPECT-- -caught: boom +outer caught: boom diff --git a/Zend/tests/generics/syntax/instanceof_with_args.phpt b/Zend/tests/generics/syntax/instanceof_with_args.phpt index 5e303b14bb6a..e98242f11ac5 100644 --- a/Zend/tests/generics/syntax/instanceof_with_args.phpt +++ b/Zend/tests/generics/syntax/instanceof_with_args.phpt @@ -1,15 +1,17 @@ --TEST-- -Generic syntax: instanceof with type arguments (args discarded at runtime) +Generic syntax: instanceof with type arguments resolves to the monomorph canonical name --FILE-- and C do not exist +// as classes, so instanceof returns false. var_dump($c instanceof C); var_dump($c instanceof C); $x = new stdClass; var_dump($x instanceof C); ?> --EXPECT-- -bool(true) -bool(true) +bool(false) +bool(false) bool(false) diff --git a/Zend/tests/generics/turbofish/instanceof_with_args.phpt b/Zend/tests/generics/turbofish/instanceof_with_args.phpt index 3da4129537b5..2e636db546ab 100644 --- a/Zend/tests/generics/turbofish/instanceof_with_args.phpt +++ b/Zend/tests/generics/turbofish/instanceof_with_args.phpt @@ -1,15 +1,24 @@ --TEST-- -Turbofish: instanceof discards type arguments +Turbofish: instanceof with type arguments compares against the monomorph canonical name --FILE-- {} +class B {} class C {} +class Animal {} +class Dog {} + $c = new C; -var_dump($c instanceof C); -var_dump($c instanceof C); -var_dump($c instanceof C>); +var_dump($c instanceof C); // true +var_dump($c instanceof C); // false: C is non-generic; C does not exist + +$b = new B::(); +var_dump($b instanceof B); // true: every mono extends the bare class +var_dump($b instanceof B); // true: same canonical mono +var_dump($b instanceof B); // false: distinct mono ?> --EXPECT-- bool(true) +bool(false) bool(true) bool(true) +bool(false) diff --git a/Zend/zend.h b/Zend/zend.h index dcaf46ad462f..509dcc593577 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -230,6 +230,7 @@ struct _zend_class_entry { struct _zend_generic_parameter_list *generic_parameters; struct _zend_generic_type_table *generic_types; + struct _zend_type_arg_table *generic_type_args; /* bindings on a monomorph; NULL on bases */ union { struct { diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index ff312155f60c..79c079b65acb 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -726,6 +726,84 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( return table; } +/* Run PHP's normal arg-type machinery (coerce in weak mode, throw TypeError + * in strict mode) against the per-call substituted type for any value-parameter + * whose pre-erasure type is a direct TYPE_PARAMETER reference. Returns false + * and sets EG(exception) on the first failure. Only bare T at the top level is + * substituted in this pass — composite shapes (array, Box, ?T) fall back + * to the erased bound check at RECV. */ +ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box) +{ + if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return true; + } + const zend_function *fbc = call->func; + if (!ZEND_USER_CODE(fbc->common.type) + || !fbc->op_array.generic_types + || !fbc->op_array.generic_types->parameters) { + return true; + } + /* Strict-types is determined by the caller's own declare; that's the + * function whose VERIFY opcode is currently running. */ + zend_execute_data *caller_ed = EG(current_execute_data); + bool strict = caller_ed + && caller_ed->func + && (caller_ed->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + const HashTable *pre = fbc->op_array.generic_types->parameters; + uint32_t num_args = ZEND_CALL_NUM_ARGS(call); + zend_ulong arg_idx; + zend_type *pe_type_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + if (arg_idx >= num_args) continue; + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= nwa->count) continue; + zend_type substituted = nwa->args[ref->index]; + if (!ZEND_TYPE_IS_SET(substituted)) continue; + zval *arg = ZEND_CALL_ARG(call, (uint32_t) arg_idx + 1); + zval *target = arg; + const zend_reference *zref = NULL; + if (Z_ISREF_P(target)) { + zref = Z_REF_P(target); + target = Z_REFVAL_P(target); + } + if (ZEND_TYPE_CONTAINS_CODE(substituted, Z_TYPE_P(target))) { + continue; + } + bool ok; + if (ZEND_TYPE_HAS_NAME(substituted) && Z_TYPE_P(target) == IS_OBJECT) { + zend_class_entry *ce = zend_lookup_class_ex(ZEND_TYPE_NAME(substituted), NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + ok = ce && instanceof_function(Z_OBJCE_P(target), ce); + } else if (zref && ZEND_REF_HAS_TYPE_SOURCES(zref)) { + ok = false; + } else { + uint32_t type_mask = ZEND_TYPE_FULL_MASK(substituted); + ok = zend_verify_scalar_type_hint(type_mask, target, strict, /* is_internal_arg */ false); + } + if (!ok) { + zend_string *expected = zend_type_to_string(substituted); + const zend_arg_info *ai = (arg_idx < fbc->common.num_args) + ? &fbc->common.arg_info[arg_idx] : NULL; + zend_throw_error(zend_ce_type_error, + "%s%s%s(): Argument #%u%s%s%s must be of type %s, %s given", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + (uint32_t) arg_idx + 1, + (ai && ai->name) ? " ($" : "", + (ai && ai->name) ? ZSTR_VAL(ai->name) : "", + (ai && ai->name) ? ")" : "", + ZSTR_VAL(expected), zend_zval_value_name(target)); + zend_string_release(expected); + return false; + } + } ZEND_HASH_FOREACH_END(); + return true; +} + ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box) { const zend_generic_parameter_list *params = NULL; @@ -1987,21 +2065,6 @@ static zend_string *zend_resolve_class_name_ast(zend_ast *ast) /* {{{ */ zend_error_noreturn(E_COMPILE_ERROR, "Illegal class name"); } - /* Class-level generic params remain bound-erased and have no runtime - * representation; function-level params are routed to a TYPE_PARAM fetch - * by zend_compile_class_ref before reaching here. */ - if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { - zend_generic_origin origin; - zend_generic_parameter *param = zend_generic_lookup_full( - Z_STR_P(class_name), &origin, NULL); - if (param && origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { - zend_error_noreturn(E_COMPILE_ERROR, - "Cannot use class-level generic type parameter %s as a class reference at runtime; " - "bound-erased generic types have no runtime representation", - Z_STRVAL_P(class_name)); - } - } - return zend_resolve_class_name(Z_STR_P(class_name), ast->attr); } /* }}} */ @@ -2829,14 +2892,11 @@ static bool zend_try_compile_const_expr_resolve_class_name(zval *zv, zend_ast *c zend_error_noreturn(E_COMPILE_ERROR, "Illegal class name"); } - /* Function-level T::class resolves at runtime through the frame's T-table. */ - if ((class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { - zend_generic_origin origin; - zend_generic_parameter *param = zend_generic_lookup_full( - Z_STR_P(class_name), &origin, NULL); - if (param && origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { - return false; - } + /* T::class resolves at runtime through the runtime T-table (frame for + * function-level T, called scope for class-level T). */ + if ((class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ + && zend_generic_lookup(Z_STR_P(class_name))) { + return false; } fetch_type = zend_get_class_fetch_type(Z_STR_P(class_name)); @@ -3140,6 +3200,7 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand ce->backed_enum_table = NULL; ce->generic_parameters = NULL; ce->generic_types = NULL; + ce->generic_type_args = NULL; if (nullify_handlers) { ce->constructor = NULL; @@ -3917,8 +3978,20 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f { uint32_t fetch_type; - /* Generic named type: discard type arguments and resolve the bare name. */ + /* Generic named type Box: when the args are concrete (no T-refs) + * we resolve to the monomorph's canonical name "Box" so callers + * like `instanceof Box` and `catch (Box $e)` distinguish + * monos. With T-refs we still need a runtime path (not yet implemented), + * so fall through to the bare-name behavior. */ if (name_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type ty = zend_compile_pre_erasure_typename(name_ast); + if (!zend_type_contains_type_parameter(ty)) { + result->op_type = IS_CONST; + ZVAL_STR(&result->u.constant, zend_type_to_canonical_string(ty)); + zend_type_release(ty, /* persistent */ false); + return; + } + zend_type_release(ty, /* persistent */ false); name_ast = name_ast->child[0]; } @@ -3961,17 +4034,18 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f return; } - /* Function/method-level T in expression position (new T(), T::FOO, - * instanceof T, T::method()) resolves through the call frame's T-table - * at runtime. */ + /* T in expression position (new T(), T::FOO, instanceof T, T::method()) + * resolves through the runtime T-table — function/method-level T from the + * call frame, class-level T from the called scope's monomorph args. */ if ((name_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { zend_generic_origin origin; uint32_t param_index = 0; zend_generic_parameter *param = zend_generic_lookup_full( zend_ast_get_str(name_ast), &origin, ¶m_index); - if (param && origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + if (param) { result->op_type = IS_UNUSED; - result->u.op.num = zend_pack_type_param_fetch(param_index, fetch_flags); + result->u.op.num = zend_pack_type_param_fetch(param_index, fetch_flags, + origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE); return; } } @@ -8271,20 +8345,6 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ zend_error_noreturn(E_COMPILE_ERROR, "Bad class name in the catch statement"); } - /* catch needs a class to check at throw time; we don't currently - * resolve generic type parameters through the catch path. */ - if (class_ast->kind == ZEND_AST_ZVAL) { - zend_ast *bare = class_ast; - if (bare->kind == ZEND_AST_GENERIC_NAMED_TYPE) bare = bare->child[0]; - if ((bare->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ - && zend_generic_lookup(zend_ast_get_str(bare))) { - zend_error_noreturn(E_COMPILE_ERROR, - "Cannot use generic type parameter %s as a class reference at runtime; " - "bound-erased generic types have no runtime representation", - ZSTR_VAL(zend_ast_get_str(bare))); - } - } - opnum_catch = get_next_op_number(); if (i == 0 && j == 0) { CG(active_op_array)->try_catch_array[try_catch_offset].catch_op = opnum_catch; @@ -8292,10 +8352,43 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ opline = get_next_op(); opline->opcode = ZEND_CATCH; - opline->op1_type = IS_CONST; - opline->op1.constant = zend_add_class_name_literal( - zend_resolve_class_name_ast(class_ast)); - opline->extended_value = zend_alloc_cache_slot(); + + /* catch (T $e): resolve T at exception-throw time via the runtime + * T-table. Encoded the same way as ZEND_FETCH_CLASS — see + * zend_resolve_generic_type_param. */ + bool emitted_type_param = false; + if (class_ast->kind == ZEND_AST_ZVAL + && (class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + zend_generic_origin origin; + uint32_t param_index = 0; + zend_generic_parameter *param = zend_generic_lookup_full( + zend_ast_get_str(class_ast), &origin, ¶m_index); + if (param) { + opline->op1_type = IS_UNUSED; + opline->op1.num = zend_pack_type_param_fetch(param_index, 0, + origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE); + opline->extended_value = 0; + emitted_type_param = true; + } + } + if (!emitted_type_param) { + zend_string *resolved_name = NULL; + /* catch (Box): canonical monomorph name when args are + * concrete, matching the semantics of `instanceof Box`. */ + if (class_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type ty = zend_compile_pre_erasure_typename(class_ast); + if (!zend_type_contains_type_parameter(ty)) { + resolved_name = zend_type_to_canonical_string(ty); + } + zend_type_release(ty, /* persistent */ false); + } + if (!resolved_name) { + resolved_name = zend_resolve_class_name_ast(class_ast); + } + opline->op1_type = IS_CONST; + opline->op1.constant = zend_add_class_name_literal(resolved_name); + opline->extended_value = zend_alloc_cache_slot(); + } if (var_name && zend_string_equals(var_name, ZSTR_KNOWN(ZEND_STR_THIS))) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot re-assign $this"); @@ -12872,8 +12965,9 @@ static void zend_compile_class_name(znode *result, const zend_ast *ast) /* {{{ * uint32_t param_index = 0; zend_generic_parameter *param = zend_generic_lookup_full( zend_ast_get_str(class_ast), &origin, ¶m_index); - if (param && origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { - op1_num = zend_pack_type_param_fetch(param_index, 0); + if (param) { + op1_num = zend_pack_type_param_fetch(param_index, 0, + origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE); } } opline->op1.num = op1_num; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 526f0987c114..4f03f548a438 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -215,6 +215,7 @@ ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type); +ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box); typedef union _zend_parser_stack_elem { zend_ast *ast; @@ -1163,13 +1164,20 @@ ZEND_API bool zend_type_contains_type_parameter(zend_type type); #define ZEND_FETCH_CLASS_AUTO 4 #define ZEND_FETCH_CLASS_INTERFACE 5 #define ZEND_FETCH_CLASS_TRAIT 6 -#define ZEND_FETCH_CLASS_TYPE_PARAM 7 +#define ZEND_FETCH_CLASS_TYPE_PARAM 7 /* function/method-level T */ +#define ZEND_FETCH_CLASS_TYPE_PARAM_CLASS 8 /* class-level T (read from called scope's mono args) */ #define ZEND_FETCH_CLASS_MASK 0x0f #define ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT 16 -static zend_always_inline uint32_t zend_pack_type_param_fetch(uint32_t param_index, uint32_t flags) { +static zend_always_inline bool zend_fetch_is_type_param(uint32_t fetch_type) { + uint32_t sub = fetch_type & ZEND_FETCH_CLASS_MASK; + return sub == ZEND_FETCH_CLASS_TYPE_PARAM || sub == ZEND_FETCH_CLASS_TYPE_PARAM_CLASS; +} + +static zend_always_inline uint32_t zend_pack_type_param_fetch(uint32_t param_index, uint32_t flags, bool class_level) { ZEND_ASSERT(param_index < (1u << (32 - ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT))); - return ZEND_FETCH_CLASS_TYPE_PARAM | (param_index << ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT) | flags; + uint32_t code = class_level ? ZEND_FETCH_CLASS_TYPE_PARAM_CLASS : ZEND_FETCH_CLASS_TYPE_PARAM; + return code | (param_index << ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT) | flags; } static zend_always_inline uint32_t zend_unpack_type_param_index(uint32_t fetch_type) { diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index a6b830357896..ad6f32b9e6ac 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1737,27 +1737,61 @@ static ZEND_COLD void report_class_fetch_error(const zend_string *class_name, ui ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type) { zend_execute_data *ex = EG(current_execute_data); - zend_type_arg_table *table = ex ? ex->type_args : NULL; + bool class_level = (fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM_CLASS; + zend_type_arg_table *table = NULL; + zend_generic_parameter_list *params = NULL; + + if (class_level) { + /* Class-level T's binding lives on the *monomorph* of the lexical + * scope — that may BE the called scope (`new Box::()`) or live + * deeper in the chain (`class IntBox extends Box`). Walk up + * from the called scope until we hit a direct child of the lexical + * scope, which is the mono that holds the binding. */ + zend_class_entry *lexical = ex ? ex->func->common.scope : NULL; + zend_class_entry *cur = ex ? zend_get_called_scope(ex) : NULL; + while (cur && cur->parent != lexical) { + cur = cur->parent; + } + if (cur) { + table = cur->generic_type_args; + } + if (lexical) { + params = lexical->generic_parameters; + } + } else if (ex && ZEND_USER_CODE(ex->func->type)) { + table = ex->type_args; + params = ex->func->op_array.generic_parameters; + } + if (table && param_index < table->count && table->names[param_index]) { return zend_fetch_class_by_name(table->names[param_index], NULL, fetch_type); } - if (ex && ZEND_USER_CODE(ex->func->type)) { - zend_generic_parameter_list *params = ex->func->op_array.generic_parameters; - if (params && param_index < params->count) { - zend_type bound = params->parameters[param_index].bound; - if (ZEND_TYPE_HAS_NAME(bound)) { - return zend_fetch_class_by_name(ZEND_TYPE_NAME(bound), NULL, fetch_type); - } - ZEND_ASSERT(params->parameters[param_index].name); - zend_throw_or_error(fetch_type, NULL, - "Cannot resolve generic type parameter %s at runtime: no binding was supplied and its bound is not a class", - ZSTR_VAL(params->parameters[param_index].name)); + /* Resolution failed. Suppress the diagnostic when SILENT and no thrown + * exception is expected (catch path passes SILENT without EXCEPTION; the + * caller treats NULL as "no match" and proceeds). */ + bool suppress = (fetch_type & ZEND_FETCH_CLASS_SILENT) + && !(fetch_type & ZEND_FETCH_CLASS_EXCEPTION); + + if (params && param_index < params->count) { + zend_type bound = params->parameters[param_index].bound; + if (ZEND_TYPE_HAS_NAME(bound)) { + return zend_fetch_class_by_name(ZEND_TYPE_NAME(bound), NULL, fetch_type); + } + if (suppress) { return NULL; } + ZEND_ASSERT(params->parameters[param_index].name); + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve generic type parameter %s at runtime: no binding was supplied and its bound is not a class", + ZSTR_VAL(params->parameters[param_index].name)); + return NULL; } - zend_throw_or_error(fetch_type, NULL, "Cannot resolve generic type parameter at runtime: not in a generic function call"); + if (suppress) { + return NULL; + } + zend_throw_or_error(fetch_type, NULL, "Cannot resolve generic type parameter at runtime: not in a generic function or class scope"); return NULL; } @@ -1769,6 +1803,7 @@ zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type) check_fetch_type: switch (fetch_sub_type) { case ZEND_FETCH_CLASS_TYPE_PARAM: + case ZEND_FETCH_CLASS_TYPE_PARAM_CLASS: return zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); case ZEND_FETCH_CLASS_SELF: diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 11838047007f..6267225a548a 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -6515,6 +6515,13 @@ ZEND_API zend_class_entry *zend_synthesize_monomorph( ce->info.user.line_end = base->info.user.line_end; ce->name = canonical; + /* Stash the bindings the runtime needs when method bodies reference the + * class-level T directly (e.g. `new T()` inside `class Box`'s body). */ + ce->generic_type_args = zend_type_arg_table_alloc(arity); + for (uint32_t i = 0; i < arity; i++) { + ce->generic_type_args->names[i] = zend_type_arg_canonical_name(args[i]); + } + bool base_is_interface = (base->ce_flags & ZEND_ACC_INTERFACE) != 0; bool base_is_trait = (base->ce_flags & ZEND_ACC_TRAIT) != 0; if (base_is_interface) { diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 2dc4abd9a05b..4a8c92888d07 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -533,6 +533,13 @@ ZEND_API void destroy_zend_class(zval *zv) p++; } } + /* Monomorph args are heap-allocated per request, not relocated into + * the opcache file cache; free them even when the rest of the ce is + * cache-resident. */ + if (ce->generic_type_args) { + zend_type_arg_table_destroy(ce->generic_type_args); + ce->generic_type_args = NULL; + } return; } @@ -653,6 +660,9 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->generic_types) { zend_generic_type_table_destroy(ce->generic_types); } + if (ce->generic_type_args) { + zend_type_arg_table_destroy(ce->generic_type_args); + } break; case ZEND_INTERNAL_CLASS: if (ce->doc_comment) { diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c index ab8f2c2b54f8..d51ad7c0356e 100644 --- a/Zend/zend_operators.c +++ b/Zend/zend_operators.c @@ -2560,6 +2560,56 @@ ZEND_API bool ZEND_FASTCALL zend_class_implements_interface(const zend_class_ent } /* }}} */ +/* Variance-aware comparison of two siblings monos `a` and `b` of the same + * generic base. Returns true if `a` is a subtype of `b` under the base's + * declared variance markers. Both monos must have generic_type_args populated + * with the same arity as base->generic_parameters. */ +static bool zend_mono_subtype_under_variance(const zend_class_entry *base, + const zend_class_entry *a, const zend_class_entry *b) +{ + if (!base->generic_parameters + || !a->generic_type_args + || !b->generic_type_args) { + return false; + } + uint32_t count = base->generic_parameters->count; + if (a->generic_type_args->count != count + || b->generic_type_args->count != count) { + return false; + } + for (uint32_t i = 0; i < count; i++) { + zend_string *a_name = a->generic_type_args->names[i]; + zend_string *b_name = b->generic_type_args->names[i]; + if (!a_name || !b_name) { + return false; + } + if (zend_string_equals(a_name, b_name)) { + continue; + } + zend_generic_variance variance = base->generic_parameters->parameters[i].variance; + if (variance == ZEND_GENERIC_VARIANCE_INVARIANT) { + return false; + } + zend_class_entry *a_ce = zend_lookup_class_ex(a_name, NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + zend_class_entry *b_ce = zend_lookup_class_ex(b_name, NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (!a_ce || !b_ce) { + return false; + } + bool ok; + if (variance == ZEND_GENERIC_VARIANCE_COVARIANT) { + ok = instanceof_function(a_ce, b_ce); + } else { /* CONTRAVARIANT */ + ok = instanceof_function(b_ce, a_ce); + } + if (!ok) { + return false; + } + } + return true; +} + ZEND_API bool ZEND_FASTCALL instanceof_function_slow(const zend_class_entry *instance_ce, const zend_class_entry *ce) /* {{{ */ { ZEND_ASSERT(instance_ce != ce && "Should have been checked already"); @@ -2576,12 +2626,24 @@ ZEND_API bool ZEND_FASTCALL instanceof_function_slow(const zend_class_entry *ins } return 0; } else { + /* When the target is a monomorph (e.g. Box), the instance may + * be a sibling monomorph of the same base (Box) — siblings via + * the parent chain are not subtypes via `==`, so check the variance + * markers on the shared base. */ + const zend_class_entry *target_base = ce->generic_type_args ? ce->parent : NULL; + const zend_class_entry *walker = instance_ce; while (1) { - instance_ce = instance_ce->parent; - if (instance_ce == ce) { + if (target_base + && walker->parent == target_base + && walker->generic_type_args + && zend_mono_subtype_under_variance(target_base, walker, ce)) { + return 1; + } + walker = walker->parent; + if (walker == ce) { return 1; } - if (instance_ce == NULL) { + if (walker == NULL) { return 0; } } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index d2897d349113..58c4b0e2e297 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4848,7 +4848,7 @@ ZEND_VM_COLD_CONST_HANDLER(108, ZEND_THROW, CONST|TMP|CV, ANY) HANDLE_EXCEPTION(); } -ZEND_VM_HANDLER(107, ZEND_CATCH, CONST, JMP_ADDR, LAST_CATCH|CACHE_SLOT) +ZEND_VM_HANDLER(107, ZEND_CATCH, CONST|UNUSED, JMP_ADDR, LAST_CATCH|CACHE_SLOT) { USE_OPLINE zend_class_entry *ce, *catch_ce; @@ -4859,11 +4859,20 @@ ZEND_VM_HANDLER(107, ZEND_CATCH, CONST, JMP_ADDR, LAST_CATCH|CACHE_SLOT) if (EG(exception) == NULL) { ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } - catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); - if (UNEXPECTED(catch_ce == NULL)) { - catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (OP1_TYPE == IS_UNUSED) { + /* catch (T $e): T resolved per execution against the runtime T-table + * — not cached, since the same opline runs in monos with different + * bindings. */ + catch_ce = zend_resolve_generic_type_param( + zend_unpack_type_param_index(opline->op1.num), + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); - CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } } ce = EG(exception)->ce; @@ -9008,6 +9017,7 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) } call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -9033,6 +9043,11 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) } if (UNEXPECTED(EG(exception))) { + /* Args have already been pushed by the SEND opcodes preceding the + * VERIFY emission for call kind; release them so refcounted values + * don't leak. */ + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { zend_string_release_ex(call->func->common.function_name, 0); zend_free_trampoline(call->func); @@ -9113,7 +9128,7 @@ ZEND_VM_HANDLER(157, ZEND_FETCH_CLASS_NAME, CV|TMP|UNUSED|CLASS_FETCH, ANY) } fetch_type = opline->op1.num; - if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + if (zend_fetch_is_type_param(fetch_type)) { SAVE_OPLINE(); zend_class_entry *resolved = zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index b5cd5b286058..aad1aed1e214 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5190,11 +5190,20 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_CO if (EG(exception) == NULL) { ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } - catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); - if (UNEXPECTED(catch_ce == NULL)) { - catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (IS_CONST == IS_UNUSED) { + /* catch (T $e): T resolved per execution against the runtime T-table + * — not cached, since the same opline runs in monos with different + * bindings. */ + catch_ce = zend_resolve_generic_type_param( + zend_unpack_type_param_index(opline->op1.num), + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); - CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } } ce = EG(exception)->ce; @@ -18306,7 +18315,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; - if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + if (zend_fetch_is_type_param(fetch_type)) { SAVE_OPLINE(); zend_class_entry *resolved = zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); @@ -22082,6 +22091,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -22107,6 +22117,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } if (UNEXPECTED(EG(exception))) { + /* Args have already been pushed by the SEND opcodes preceding the + * VERIFY emission for call kind; release them so refcounted values + * don't leak. */ + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { zend_string_release_ex(call->func->common.function_name, 0); zend_free_trampoline(call->func); @@ -32926,6 +32941,65 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_F } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_class_entry *ce, *catch_ce; + zend_object *exception; + + SAVE_OPLINE(); + /* Check whether an exception has been thrown, if not, jump over code */ + if (EG(exception) == NULL) { + ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); + } + if (IS_UNUSED == IS_UNUSED) { + /* catch (T $e): T resolved per execution against the runtime T-table + * — not cached, since the same opline runs in monos with different + * bindings. */ + catch_ce = zend_resolve_generic_type_param( + zend_unpack_type_param_index(opline->op1.num), + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } + } + ce = EG(exception)->ce; + +#ifdef HAVE_DTRACE + if (DTRACE_EXCEPTION_CAUGHT_ENABLED()) { + DTRACE_EXCEPTION_CAUGHT((char *)ce->name); + } +#endif /* HAVE_DTRACE */ + + if (ce != catch_ce) { + if (!catch_ce || !instanceof_function(ce, catch_ce)) { + if (opline->extended_value & ZEND_LAST_CATCH) { + zend_rethrow_exception(execute_data); + HANDLE_EXCEPTION(); + } + ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); + } + } + + exception = EG(exception); + EG(exception) = NULL; + if (RETURN_VALUE_USED(opline)) { + /* Always perform a strict assignment. There is a reasonable expectation that if you + * write "catch (Exception $e)" then $e will actually be instanceof Exception. As such, + * we should not permit coercion to string here. */ + zval tmp; + ZVAL_OBJ(&tmp, exception); + zend_assign_to_variable(EX_VAR(opline->result.var), &tmp, IS_TMP_VAR, /* strict */ 1); + } else { + OBJ_RELEASE(exception); + } + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -33021,7 +33095,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; - if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + if (zend_fetch_is_type_param(fetch_type)) { SAVE_OPLINE(); zend_class_entry *resolved = zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); @@ -37545,6 +37619,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -37570,6 +37645,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } if (UNEXPECTED(EG(exception))) { + /* Args have already been pushed by the SEND opcodes preceding the + * VERIFY emission for call kind; release them so refcounted values + * don't leak. */ + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { zend_string_release_ex(call->func->common.function_name, 0); zend_free_trampoline(call->func); @@ -41138,7 +41218,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; - if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + if (zend_fetch_is_type_param(fetch_type)) { SAVE_OPLINE(); zend_class_entry *resolved = zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); @@ -58166,11 +58246,20 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_CONST_T if (EG(exception) == NULL) { ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } - catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); - if (UNEXPECTED(catch_ce == NULL)) { - catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (IS_CONST == IS_UNUSED) { + /* catch (T $e): T resolved per execution against the runtime T-table + * — not cached, since the same opline runs in monos with different + * bindings. */ + catch_ce = zend_resolve_generic_type_param( + zend_unpack_type_param_index(opline->op1.num), + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); - CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } } ce = EG(exception)->ce; @@ -71180,7 +71269,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; - if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + if (zend_fetch_is_type_param(fetch_type)) { SAVE_OPLINE(); zend_class_entry *resolved = zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); @@ -74856,6 +74945,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -74881,6 +74971,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } if (UNEXPECTED(EG(exception))) { + /* Args have already been pushed by the SEND opcodes preceding the + * VERIFY emission for call kind; release them so refcounted values + * don't leak. */ + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { zend_string_release_ex(call->func->common.function_name, 0); zend_free_trampoline(call->func); @@ -85700,6 +85795,65 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FE_FET } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_class_entry *ce, *catch_ce; + zend_object *exception; + + SAVE_OPLINE(); + /* Check whether an exception has been thrown, if not, jump over code */ + if (EG(exception) == NULL) { + ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); + } + if (IS_UNUSED == IS_UNUSED) { + /* catch (T $e): T resolved per execution against the runtime T-table + * — not cached, since the same opline runs in monos with different + * bindings. */ + catch_ce = zend_resolve_generic_type_param( + zend_unpack_type_param_index(opline->op1.num), + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } + } + ce = EG(exception)->ce; + +#ifdef HAVE_DTRACE + if (DTRACE_EXCEPTION_CAUGHT_ENABLED()) { + DTRACE_EXCEPTION_CAUGHT((char *)ce->name); + } +#endif /* HAVE_DTRACE */ + + if (ce != catch_ce) { + if (!catch_ce || !instanceof_function(ce, catch_ce)) { + if (opline->extended_value & ZEND_LAST_CATCH) { + zend_rethrow_exception(execute_data); + HANDLE_EXCEPTION(); + } + ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); + } + } + + exception = EG(exception); + EG(exception) = NULL; + if (RETURN_VALUE_USED(opline)) { + /* Always perform a strict assignment. There is a reasonable expectation that if you + * write "catch (Exception $e)" then $e will actually be instanceof Exception. As such, + * we should not permit coercion to string here. */ + zval tmp; + ZVAL_OBJ(&tmp, exception); + zend_assign_to_variable(EX_VAR(opline->result.var), &tmp, IS_TMP_VAR, /* strict */ 1); + } else { + OBJ_RELEASE(exception); + } + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CLONE_SPEC_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -85795,7 +85949,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; - if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + if (zend_fetch_is_type_param(fetch_type)) { SAVE_OPLINE(); zend_class_entry *resolved = zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); @@ -90319,6 +90473,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -90344,6 +90499,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } if (UNEXPECTED(EG(exception))) { + /* Args have already been pushed by the SEND opcodes preceding the + * VERIFY emission for call kind; release them so refcounted values + * don't leak. */ + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { zend_string_release_ex(call->func->common.function_name, 0); zend_free_trampoline(call->func); @@ -93912,7 +94072,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; - if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM) { + if (zend_fetch_is_type_param(fetch_type)) { SAVE_OPLINE(); zend_class_entry *resolved = zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); @@ -109255,6 +109415,10 @@ ZEND_API void execute_ex(zend_execute_data *ex) (void*)&&ZEND_SEND_VAR_NO_REF_SPEC_VAR_UNUSED_LABEL, (void*)&&ZEND_NULL_LABEL, (void*)&&ZEND_CATCH_SPEC_CONST_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_CATCH_SPEC_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, (void*)&&ZEND_THROW_SPEC_CONST_LABEL, (void*)&&ZEND_THROW_SPEC_TMP_LABEL, (void*)&&ZEND_NULL_LABEL, @@ -114394,6 +114558,11 @@ ZEND_API void execute_ex(zend_execute_data *ex) ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); VM_TRACE_OP_END(ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) HYBRID_BREAK(); + HYBRID_CASE(ZEND_CATCH_SPEC_UNUSED): + VM_TRACE(ZEND_CATCH_SPEC_UNUSED) + ZEND_CATCH_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_CATCH_SPEC_UNUSED) + HYBRID_BREAK(); HYBRID_CASE(ZEND_CLONE_SPEC_UNUSED): VM_TRACE(ZEND_CLONE_SPEC_UNUSED) ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); @@ -118208,6 +118377,10 @@ void zend_vm_init(void) ZEND_SEND_VAR_NO_REF_SPEC_VAR_UNUSED_HANDLER, ZEND_NULL_HANDLER, ZEND_CATCH_SPEC_CONST_HANDLER, + ZEND_NULL_HANDLER, + ZEND_NULL_HANDLER, + ZEND_CATCH_SPEC_UNUSED_HANDLER, + ZEND_NULL_HANDLER, ZEND_THROW_SPEC_CONST_HANDLER, ZEND_THROW_SPEC_TMP_HANDLER, ZEND_NULL_HANDLER, @@ -121691,6 +121864,10 @@ void zend_vm_init(void) ZEND_SEND_VAR_NO_REF_SPEC_VAR_UNUSED_TAILCALL_HANDLER, ZEND_NULL_TAILCALL_HANDLER, ZEND_CATCH_SPEC_CONST_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_CATCH_SPEC_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, ZEND_THROW_SPEC_CONST_TAILCALL_HANDLER, ZEND_THROW_SPEC_TMP_TAILCALL_HANDLER, ZEND_NULL_TAILCALL_HANDLER, @@ -123256,7 +123433,7 @@ void zend_vm_init(void) 1255, 1256 | SPEC_RULE_OP1, 1261 | SPEC_RULE_OP1, - 3479, + 3483, 1266 | SPEC_RULE_OP1, 1271 | SPEC_RULE_OP1, 1276 | SPEC_RULE_OP2, @@ -123290,7 +123467,7 @@ void zend_vm_init(void) 1559 | SPEC_RULE_OP1 | SPEC_RULE_OP2, 1584 | SPEC_RULE_OP1, 1589, - 3479, + 3483, 1590 | SPEC_RULE_OP1, 1595 | SPEC_RULE_OP1 | SPEC_RULE_OP2, 1620 | SPEC_RULE_OP1 | SPEC_RULE_OP2, @@ -123318,155 +123495,155 @@ void zend_vm_init(void) 1959, 1960, 1961 | SPEC_RULE_OP2, - 1966, - 1967 | SPEC_RULE_OP1, - 1972 | SPEC_RULE_OP2, - 1977 | SPEC_RULE_OP1, - 1982 | SPEC_RULE_OP1 | SPEC_RULE_OBSERVER, - 1992 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2017 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2042 | SPEC_RULE_OP1, - 2047 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2072 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_QUICK_ARG, - 2122 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2147 | SPEC_RULE_OP2, - 2152, - 2153 | SPEC_RULE_OP1, - 2158 | SPEC_RULE_OP1, - 2163, - 2164 | SPEC_RULE_OP1, - 2169 | SPEC_RULE_OP1, - 2174 | SPEC_RULE_OP1, - 2179, - 2180, - 2181 | SPEC_RULE_OP2, - 2186 | SPEC_RULE_RETVAL | SPEC_RULE_OBSERVER, + 1966 | SPEC_RULE_OP1, + 1971 | SPEC_RULE_OP1, + 1976 | SPEC_RULE_OP2, + 1981 | SPEC_RULE_OP1, + 1986 | SPEC_RULE_OP1 | SPEC_RULE_OBSERVER, + 1996 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2021 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2046 | SPEC_RULE_OP1, + 2051 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2076 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_QUICK_ARG, + 2126 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2151 | SPEC_RULE_OP2, + 2156, + 2157 | SPEC_RULE_OP1, + 2162 | SPEC_RULE_OP1, + 2167, + 2168 | SPEC_RULE_OP1, + 2173 | SPEC_RULE_OP1, + 2178 | SPEC_RULE_OP1, + 2183, + 2184, + 2185 | SPEC_RULE_OP2, 2190 | SPEC_RULE_RETVAL | SPEC_RULE_OBSERVER, 2194 | SPEC_RULE_RETVAL | SPEC_RULE_OBSERVER, - 2198 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2198 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2223 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2223 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2248 | SPEC_RULE_OP1, - 2253, - 2254 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2279, - 2280 | SPEC_RULE_OP1, - 2285, - 2286, - 2287, - 2288, + 2198 | SPEC_RULE_RETVAL | SPEC_RULE_OBSERVER, + 2202 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2202 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2227 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2227 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2252 | SPEC_RULE_OP1, + 2257, + 2258 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2283, + 2284 | SPEC_RULE_OP1, 2289, 2290, 2291, - 2292 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2317, - 2318, - 2319, - 2320 | SPEC_RULE_OP1, - 2325, - 2326 | SPEC_RULE_ISSET, - 2328 | SPEC_RULE_OP2, - 2333, - 2334 | SPEC_RULE_OP1, - 2339 | SPEC_RULE_OBSERVER, - 2341, - 2342 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2367 | SPEC_RULE_OP1 | SPEC_RULE_OBSERVER, - 2377, - 2378, - 2379, - 2380, - 2381 | SPEC_RULE_OP1, - 2386, - 2387, - 2388 | SPEC_RULE_OP1, - 2393 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2418, - 2419 | SPEC_RULE_OP1, - 2424, - 2425, - 2426, - 2427, + 2292, + 2293, + 2294, + 2295, + 2296 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2321, + 2322, + 2323, + 2324 | SPEC_RULE_OP1, + 2329, + 2330 | SPEC_RULE_ISSET, + 2332 | SPEC_RULE_OP2, + 2337, + 2338 | SPEC_RULE_OP1, + 2343 | SPEC_RULE_OBSERVER, + 2345, + 2346 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2371 | SPEC_RULE_OP1 | SPEC_RULE_OBSERVER, + 2381, + 2382, + 2383, + 2384, + 2385 | SPEC_RULE_OP1, + 2390, + 2391, + 2392 | SPEC_RULE_OP1, + 2397 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2422, + 2423 | SPEC_RULE_OP1, 2428, 2429, 2430, 2431, - 2432 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2457, - 2458, - 2459, - 2460 | SPEC_RULE_OP2, - 2465, - 2466 | SPEC_RULE_OP1, - 2471 | SPEC_RULE_OP1, - 2476 | SPEC_RULE_OP1, - 2481 | SPEC_RULE_OP1, - 2486 | SPEC_RULE_OP1, - 2491, - 2492 | SPEC_RULE_OP1, - 2497 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2522 | SPEC_RULE_OP1, - 2527 | SPEC_RULE_OP2, - 2532 | SPEC_RULE_OP1, - 2537 | SPEC_RULE_OP1, - 2542, - 2543, - 2544, - 2545, + 2432, + 2433, + 2434, + 2435, + 2436 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2461, + 2462, + 2463, + 2464 | SPEC_RULE_OP2, + 2469, + 2470 | SPEC_RULE_OP1, + 2475 | SPEC_RULE_OP1, + 2480 | SPEC_RULE_OP1, + 2485 | SPEC_RULE_OP1, + 2490 | SPEC_RULE_OP1, + 2495, + 2496 | SPEC_RULE_OP1, + 2501 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2526 | SPEC_RULE_OP1, + 2531 | SPEC_RULE_OP2, + 2536 | SPEC_RULE_OP1, + 2541 | SPEC_RULE_OP1, 2546, - 2547 | SPEC_RULE_OBSERVER, - 2549 | SPEC_RULE_OBSERVER, + 2547, + 2548, + 2549, + 2550, 2551 | SPEC_RULE_OBSERVER, 2553 | SPEC_RULE_OBSERVER, - 2555, - 2556, - 2557, - 2558, - 2559 | SPEC_RULE_OP1, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, - 3479, + 2555 | SPEC_RULE_OBSERVER, + 2557 | SPEC_RULE_OBSERVER, + 2559, + 2560, + 2561, + 2562, + 2563 | SPEC_RULE_OP1, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, + 3483, }; #if 0 #elif (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) @@ -123659,7 +123836,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2572 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2576 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123667,7 +123844,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2597 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2601 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123675,7 +123852,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2622 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2626 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123686,17 +123863,17 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2647 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2651 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } else if (op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2672 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2676 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2697 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2701 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } break; case ZEND_MUL: @@ -123707,17 +123884,17 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2722 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2726 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2747 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2751 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2772 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2776 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_IDENTICAL: @@ -123728,16 +123905,16 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2797 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2801 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2872 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2876 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0)) { - spec = 3097 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3101 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { - spec = 3103 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 3107 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_NOT_IDENTICAL: @@ -123748,16 +123925,16 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2947 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2951 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3022 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3026 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0)) { - spec = 3100 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3104 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { - spec = 3108 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 3112 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_EQUAL: @@ -123768,12 +123945,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2797 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2801 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2872 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2876 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_NOT_EQUAL: @@ -123784,12 +123961,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2947 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2951 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3022 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3026 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_SMALLER: @@ -123797,12 +123974,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3113 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3117 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3188 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3192 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } break; case ZEND_IS_SMALLER_OR_EQUAL: @@ -123810,79 +123987,79 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3263 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3267 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3338 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3342 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } break; case ZEND_QM_ASSIGN: if (op1_info == MAY_BE_LONG) { - spec = 3425 | SPEC_RULE_OP1; + spec = 3429 | SPEC_RULE_OP1; } else if (op1_info == MAY_BE_DOUBLE) { - spec = 3430 | SPEC_RULE_OP1; + spec = 3434 | SPEC_RULE_OP1; } else if ((op->op1_type == IS_CONST) ? !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1)) : (!(op1_info & ((MAY_BE_ANY|MAY_BE_UNDEF)-(MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE))))) { - spec = 3435 | SPEC_RULE_OP1; + spec = 3439 | SPEC_RULE_OP1; } break; case ZEND_PRE_INC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3413 | SPEC_RULE_RETVAL; + spec = 3417 | SPEC_RULE_RETVAL; } else if (op1_info == MAY_BE_LONG) { - spec = 3415 | SPEC_RULE_RETVAL; + spec = 3419 | SPEC_RULE_RETVAL; } break; case ZEND_PRE_DEC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3417 | SPEC_RULE_RETVAL; + spec = 3421 | SPEC_RULE_RETVAL; } else if (op1_info == MAY_BE_LONG) { - spec = 3419 | SPEC_RULE_RETVAL; + spec = 3423 | SPEC_RULE_RETVAL; } break; case ZEND_POST_INC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3421; + spec = 3425; } else if (op1_info == MAY_BE_LONG) { - spec = 3422; + spec = 3426; } break; case ZEND_POST_DEC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3423; + spec = 3427; } else if (op1_info == MAY_BE_LONG) { - spec = 3424; + spec = 3428; } break; case ZEND_JMP: if (OP_JMP_ADDR(op, op->op1) > op) { - spec = 2571; + spec = 2575; } break; case ZEND_INIT_FCALL: if (Z_EXTRA_P(RT_CONSTANT(op, op->op2)) != 0) { - spec = 2564; + spec = 2568; } break; case ZEND_RECV: if (op->op2.num == MAY_BE_ANY) { - spec = 2565; + spec = 2569; } break; case ZEND_SEND_VAL: if (op->op1_type == IS_CONST && op->op2_type == IS_UNUSED && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1))) { - spec = 3475; + spec = 3479; } break; case ZEND_SEND_VAR_EX: if (op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0) { - spec = 3470 | SPEC_RULE_OP1; + spec = 3474 | SPEC_RULE_OP1; } break; case ZEND_FE_FETCH_R: if (op->op2_type == IS_CV && (op1_info & (MAY_BE_ANY|MAY_BE_REF)) == MAY_BE_ARRAY) { - spec = 3477 | SPEC_RULE_RETVAL; + spec = 3481 | SPEC_RULE_RETVAL; } break; case ZEND_FETCH_DIM_R: @@ -123890,22 +124067,22 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3440 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 3444 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } break; case ZEND_SEND_VAL_EX: if (op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && op->op1_type == IS_CONST && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1))) { - spec = 3476; + spec = 3480; } break; case ZEND_SEND_VAR: if (op->op2_type == IS_UNUSED && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0) { - spec = 3465 | SPEC_RULE_OP1; + spec = 3469 | SPEC_RULE_OP1; } break; case ZEND_COUNT: if ((op1_info & (MAY_BE_ANY|MAY_BE_UNDEF|MAY_BE_REF)) == MAY_BE_ARRAY) { - spec = 2566 | SPEC_RULE_OP1; + spec = 2570 | SPEC_RULE_OP1; } break; case ZEND_BW_OR: diff --git a/Zend/zend_vm_handlers.h b/Zend/zend_vm_handlers.h index 195a8d5b4dac..ede9bb61f95e 100644 --- a/Zend/zend_vm_handlers.h +++ b/Zend/zend_vm_handlers.h @@ -772,824 +772,825 @@ _(1961, ZEND_SEND_VAR_NO_REF_SPEC_VAR_CONST) \ _(1964, ZEND_SEND_VAR_NO_REF_SPEC_VAR_UNUSED) \ _(1966, ZEND_CATCH_SPEC_CONST) \ - _(1967, ZEND_THROW_SPEC_CONST) \ - _(1968, ZEND_THROW_SPEC_TMP) \ - _(1971, ZEND_THROW_SPEC_CV) \ - _(1972, ZEND_FETCH_CLASS_SPEC_UNUSED_CONST) \ - _(1973, ZEND_FETCH_CLASS_SPEC_UNUSED_TMP) \ - _(1975, ZEND_FETCH_CLASS_SPEC_UNUSED_UNUSED) \ - _(1976, ZEND_FETCH_CLASS_SPEC_UNUSED_CV) \ - _(1977, ZEND_CLONE_SPEC_CONST) \ - _(1978, ZEND_CLONE_SPEC_TMP) \ - _(1980, ZEND_CLONE_SPEC_UNUSED) \ - _(1981, ZEND_CLONE_SPEC_CV) \ - _(1982, ZEND_RETURN_BY_REF_SPEC_CONST) \ - _(1983, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ - _(1984, ZEND_RETURN_BY_REF_SPEC_TMP) \ - _(1985, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ - _(1986, ZEND_RETURN_BY_REF_SPEC_VAR) \ + _(1969, ZEND_CATCH_SPEC_UNUSED) \ + _(1971, ZEND_THROW_SPEC_CONST) \ + _(1972, ZEND_THROW_SPEC_TMP) \ + _(1975, ZEND_THROW_SPEC_CV) \ + _(1976, ZEND_FETCH_CLASS_SPEC_UNUSED_CONST) \ + _(1977, ZEND_FETCH_CLASS_SPEC_UNUSED_TMP) \ + _(1979, ZEND_FETCH_CLASS_SPEC_UNUSED_UNUSED) \ + _(1980, ZEND_FETCH_CLASS_SPEC_UNUSED_CV) \ + _(1981, ZEND_CLONE_SPEC_CONST) \ + _(1982, ZEND_CLONE_SPEC_TMP) \ + _(1984, ZEND_CLONE_SPEC_UNUSED) \ + _(1985, ZEND_CLONE_SPEC_CV) \ + _(1986, ZEND_RETURN_BY_REF_SPEC_CONST) \ _(1987, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ - _(1990, ZEND_RETURN_BY_REF_SPEC_CV) \ + _(1988, ZEND_RETURN_BY_REF_SPEC_TMP) \ + _(1989, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ + _(1990, ZEND_RETURN_BY_REF_SPEC_VAR) \ _(1991, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ - _(1992, ZEND_INIT_METHOD_CALL_SPEC_CONST_CONST) \ - _(1993, ZEND_INIT_METHOD_CALL_SPEC_CONST_TMP) \ - _(1996, ZEND_INIT_METHOD_CALL_SPEC_CONST_CV) \ - _(1997, ZEND_INIT_METHOD_CALL_SPEC_TMP_CONST) \ - _(1998, ZEND_INIT_METHOD_CALL_SPEC_TMP_TMP) \ - _(2001, ZEND_INIT_METHOD_CALL_SPEC_TMP_CV) \ - _(2007, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CONST) \ - _(2008, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_TMP) \ - _(2011, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CV) \ - _(2012, ZEND_INIT_METHOD_CALL_SPEC_CV_CONST) \ - _(2013, ZEND_INIT_METHOD_CALL_SPEC_CV_TMP) \ - _(2016, ZEND_INIT_METHOD_CALL_SPEC_CV_CV) \ - _(2017, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST) \ - _(2018, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_TMP) \ - _(2020, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_UNUSED) \ - _(2021, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CV) \ - _(2027, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_CONST) \ - _(2028, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_TMP) \ - _(2030, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_UNUSED) \ - _(2031, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_CV) \ - _(2032, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_CONST) \ - _(2033, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_TMP) \ - _(2035, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_UNUSED) \ - _(2036, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_CV) \ - _(2042, ZEND_ISSET_ISEMPTY_VAR_SPEC_CONST_UNUSED) \ - _(2043, ZEND_ISSET_ISEMPTY_VAR_SPEC_TMP_UNUSED) \ - _(2046, ZEND_ISSET_ISEMPTY_VAR_SPEC_CV_UNUSED) \ - _(2047, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CONST) \ - _(2048, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_TMP) \ - _(2051, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CV) \ - _(2052, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_CONST) \ - _(2053, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_TMP) \ - _(2056, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_CV) \ - _(2067, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CONST) \ - _(2068, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_TMP) \ - _(2071, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CV) \ - _(2072, ZEND_SEND_VAL_EX_SPEC_CONST_CONST) \ - _(2073, ZEND_SEND_VAL_EX_SPEC_CONST_CONST) \ - _(2078, ZEND_SEND_VAL_EX_SPEC_CONST_UNUSED) \ - _(2079, ZEND_SEND_VAL_EX_SPEC_CONST_UNUSED_QUICK) \ - _(2082, ZEND_SEND_VAL_EX_SPEC_TMP_CONST) \ - _(2083, ZEND_SEND_VAL_EX_SPEC_TMP_CONST) \ - _(2088, ZEND_SEND_VAL_EX_SPEC_TMP_UNUSED) \ - _(2089, ZEND_SEND_VAL_EX_SPEC_TMP_UNUSED_QUICK) \ - _(2132, ZEND_SEND_VAR_SPEC_VAR_CONST) \ - _(2135, ZEND_SEND_VAR_SPEC_VAR_UNUSED) \ - _(2142, ZEND_SEND_VAR_SPEC_CV_CONST) \ - _(2145, ZEND_SEND_VAR_SPEC_CV_UNUSED) \ - _(2147, ZEND_INIT_USER_CALL_SPEC_CONST_CONST) \ - _(2148, ZEND_INIT_USER_CALL_SPEC_CONST_TMP) \ - _(2151, ZEND_INIT_USER_CALL_SPEC_CONST_CV) \ - _(2152, ZEND_SEND_ARRAY_SPEC) \ - _(2153, ZEND_SEND_USER_SPEC_CONST) \ - _(2154, ZEND_SEND_USER_SPEC_TMP) \ - _(2157, ZEND_SEND_USER_SPEC_CV) \ - _(2158, ZEND_STRLEN_SPEC_CONST) \ - _(2159, ZEND_STRLEN_SPEC_TMP) \ - _(2162, ZEND_STRLEN_SPEC_CV) \ - _(2163, ZEND_DEFINED_SPEC_CONST) \ - _(2164, ZEND_TYPE_CHECK_SPEC_CONST) \ - _(2165, ZEND_TYPE_CHECK_SPEC_TMP) \ - _(2168, ZEND_TYPE_CHECK_SPEC_CV) \ - _(2169, ZEND_VERIFY_RETURN_TYPE_SPEC_CONST_UNUSED) \ - _(2170, ZEND_VERIFY_RETURN_TYPE_SPEC_TMP_UNUSED) \ - _(2171, ZEND_VERIFY_RETURN_TYPE_SPEC_VAR_UNUSED) \ - _(2172, ZEND_VERIFY_RETURN_TYPE_SPEC_UNUSED_UNUSED) \ - _(2173, ZEND_VERIFY_RETURN_TYPE_SPEC_CV_UNUSED) \ - _(2174, ZEND_FE_RESET_RW_SPEC_CONST) \ - _(2175, ZEND_FE_RESET_RW_SPEC_TMP) \ - _(2176, ZEND_FE_RESET_RW_SPEC_VAR) \ - _(2178, ZEND_FE_RESET_RW_SPEC_CV) \ - _(2179, ZEND_FE_FETCH_RW_SPEC_VAR) \ - _(2180, ZEND_FE_FREE_SPEC_TMPVAR) \ - _(2181, ZEND_INIT_DYNAMIC_CALL_SPEC_CONST) \ - _(2182, ZEND_INIT_DYNAMIC_CALL_SPEC_TMP) \ - _(2185, ZEND_INIT_DYNAMIC_CALL_SPEC_CV) \ - _(2186, ZEND_DO_ICALL_SPEC_RETVAL_UNUSED) \ - _(2187, ZEND_DO_ICALL_SPEC_RETVAL_USED) \ - _(2188, ZEND_DO_ICALL_SPEC_OBSERVER) \ - _(2189, ZEND_DO_ICALL_SPEC_OBSERVER) \ - _(2190, ZEND_DO_UCALL_SPEC_RETVAL_UNUSED) \ - _(2191, ZEND_DO_UCALL_SPEC_RETVAL_USED) \ - _(2192, ZEND_DO_UCALL_SPEC_OBSERVER) \ - _(2193, ZEND_DO_UCALL_SPEC_OBSERVER) \ - _(2194, ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_UNUSED) \ - _(2195, ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED) \ - _(2196, ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER) \ - _(2197, ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER) \ - _(2208, ZEND_PRE_INC_OBJ_SPEC_VAR_CONST) \ - _(2209, ZEND_PRE_INC_OBJ_SPEC_VAR_TMP) \ - _(2212, ZEND_PRE_INC_OBJ_SPEC_VAR_CV) \ - _(2213, ZEND_PRE_INC_OBJ_SPEC_UNUSED_CONST) \ - _(2214, ZEND_PRE_INC_OBJ_SPEC_UNUSED_TMP) \ - _(2217, ZEND_PRE_INC_OBJ_SPEC_UNUSED_CV) \ - _(2218, ZEND_PRE_INC_OBJ_SPEC_CV_CONST) \ - _(2219, ZEND_PRE_INC_OBJ_SPEC_CV_TMP) \ - _(2222, ZEND_PRE_INC_OBJ_SPEC_CV_CV) \ - _(2233, ZEND_POST_INC_OBJ_SPEC_VAR_CONST) \ - _(2234, ZEND_POST_INC_OBJ_SPEC_VAR_TMP) \ - _(2237, ZEND_POST_INC_OBJ_SPEC_VAR_CV) \ - _(2238, ZEND_POST_INC_OBJ_SPEC_UNUSED_CONST) \ - _(2239, ZEND_POST_INC_OBJ_SPEC_UNUSED_TMP) \ - _(2242, ZEND_POST_INC_OBJ_SPEC_UNUSED_CV) \ - _(2243, ZEND_POST_INC_OBJ_SPEC_CV_CONST) \ - _(2244, ZEND_POST_INC_OBJ_SPEC_CV_TMP) \ - _(2247, ZEND_POST_INC_OBJ_SPEC_CV_CV) \ - _(2248, ZEND_ECHO_SPEC_CONST) \ - _(2249, ZEND_ECHO_SPEC_TMP) \ - _(2252, ZEND_ECHO_SPEC_CV) \ - _(2259, ZEND_INSTANCEOF_SPEC_TMP_CONST) \ - _(2261, ZEND_INSTANCEOF_SPEC_TMP_VAR) \ - _(2262, ZEND_INSTANCEOF_SPEC_TMP_UNUSED) \ - _(2274, ZEND_INSTANCEOF_SPEC_CV_CONST) \ - _(2276, ZEND_INSTANCEOF_SPEC_CV_VAR) \ - _(2277, ZEND_INSTANCEOF_SPEC_CV_UNUSED) \ - _(2279, ZEND_GENERATOR_CREATE_SPEC) \ - _(2282, ZEND_MAKE_REF_SPEC_VAR_UNUSED) \ - _(2284, ZEND_MAKE_REF_SPEC_CV_UNUSED) \ - _(2285, ZEND_DECLARE_FUNCTION_SPEC) \ - _(2286, ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST) \ - _(2287, ZEND_DECLARE_CONST_SPEC_CONST_CONST) \ - _(2288, ZEND_DECLARE_CLASS_SPEC_CONST) \ - _(2289, ZEND_DECLARE_CLASS_DELAYED_SPEC_CONST_CONST) \ - _(2290, ZEND_DECLARE_ANON_CLASS_SPEC) \ - _(2291, ZEND_ADD_ARRAY_UNPACK_SPEC) \ - _(2292, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_CONST) \ - _(2293, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_TMP) \ - _(2296, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_CV) \ - _(2297, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_CONST) \ - _(2298, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_TMP) \ - _(2301, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_CV) \ - _(2307, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_CONST) \ - _(2308, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_TMP) \ - _(2311, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_CV) \ - _(2312, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CONST) \ - _(2313, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_TMP) \ - _(2316, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CV) \ - _(2317, ZEND_HANDLE_EXCEPTION_SPEC) \ - _(2318, ZEND_USER_OPCODE_SPEC) \ - _(2319, ZEND_ASSERT_CHECK_SPEC) \ - _(2320, ZEND_JMP_SET_SPEC_CONST) \ - _(2321, ZEND_JMP_SET_SPEC_TMP) \ - _(2324, ZEND_JMP_SET_SPEC_CV) \ - _(2325, ZEND_UNSET_CV_SPEC_CV_UNUSED) \ - _(2326, ZEND_ISSET_ISEMPTY_CV_SPEC_CV_UNUSED_SET) \ - _(2327, ZEND_ISSET_ISEMPTY_CV_SPEC_CV_UNUSED_EMPTY) \ - _(2328, ZEND_FETCH_LIST_W_SPEC_VAR_CONST) \ - _(2329, ZEND_FETCH_LIST_W_SPEC_VAR_TMP) \ - _(2332, ZEND_FETCH_LIST_W_SPEC_VAR_CV) \ - _(2333, ZEND_SEPARATE_SPEC_VAR_UNUSED) \ - _(2335, ZEND_FETCH_CLASS_NAME_SPEC_TMP) \ - _(2337, ZEND_FETCH_CLASS_NAME_SPEC_UNUSED) \ - _(2338, ZEND_FETCH_CLASS_NAME_SPEC_CV) \ - _(2339, ZEND_CALL_TRAMPOLINE_SPEC) \ - _(2340, ZEND_CALL_TRAMPOLINE_SPEC_OBSERVER) \ - _(2341, ZEND_DISCARD_EXCEPTION_SPEC) \ - _(2342, ZEND_YIELD_SPEC_CONST_CONST) \ - _(2343, ZEND_YIELD_SPEC_CONST_TMP) \ - _(2345, ZEND_YIELD_SPEC_CONST_UNUSED) \ - _(2346, ZEND_YIELD_SPEC_CONST_CV) \ - _(2347, ZEND_YIELD_SPEC_TMP_CONST) \ - _(2348, ZEND_YIELD_SPEC_TMP_TMP) \ - _(2350, ZEND_YIELD_SPEC_TMP_UNUSED) \ - _(2351, ZEND_YIELD_SPEC_TMP_CV) \ - _(2352, ZEND_YIELD_SPEC_VAR_CONST) \ - _(2353, ZEND_YIELD_SPEC_VAR_TMP) \ - _(2355, ZEND_YIELD_SPEC_VAR_UNUSED) \ - _(2356, ZEND_YIELD_SPEC_VAR_CV) \ - _(2357, ZEND_YIELD_SPEC_UNUSED_CONST) \ - _(2358, ZEND_YIELD_SPEC_UNUSED_TMP) \ - _(2360, ZEND_YIELD_SPEC_UNUSED_UNUSED) \ - _(2361, ZEND_YIELD_SPEC_UNUSED_CV) \ - _(2362, ZEND_YIELD_SPEC_CV_CONST) \ - _(2363, ZEND_YIELD_SPEC_CV_TMP) \ - _(2365, ZEND_YIELD_SPEC_CV_UNUSED) \ - _(2366, ZEND_YIELD_SPEC_CV_CV) \ - _(2367, ZEND_GENERATOR_RETURN_SPEC_CONST) \ - _(2368, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ - _(2369, ZEND_GENERATOR_RETURN_SPEC_TMP) \ - _(2370, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ - _(2371, ZEND_GENERATOR_RETURN_SPEC_VAR) \ + _(1994, ZEND_RETURN_BY_REF_SPEC_CV) \ + _(1995, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ + _(1996, ZEND_INIT_METHOD_CALL_SPEC_CONST_CONST) \ + _(1997, ZEND_INIT_METHOD_CALL_SPEC_CONST_TMP) \ + _(2000, ZEND_INIT_METHOD_CALL_SPEC_CONST_CV) \ + _(2001, ZEND_INIT_METHOD_CALL_SPEC_TMP_CONST) \ + _(2002, ZEND_INIT_METHOD_CALL_SPEC_TMP_TMP) \ + _(2005, ZEND_INIT_METHOD_CALL_SPEC_TMP_CV) \ + _(2011, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CONST) \ + _(2012, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_TMP) \ + _(2015, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CV) \ + _(2016, ZEND_INIT_METHOD_CALL_SPEC_CV_CONST) \ + _(2017, ZEND_INIT_METHOD_CALL_SPEC_CV_TMP) \ + _(2020, ZEND_INIT_METHOD_CALL_SPEC_CV_CV) \ + _(2021, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST) \ + _(2022, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_TMP) \ + _(2024, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_UNUSED) \ + _(2025, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CV) \ + _(2031, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_CONST) \ + _(2032, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_TMP) \ + _(2034, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_UNUSED) \ + _(2035, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_CV) \ + _(2036, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_CONST) \ + _(2037, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_TMP) \ + _(2039, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_UNUSED) \ + _(2040, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_CV) \ + _(2046, ZEND_ISSET_ISEMPTY_VAR_SPEC_CONST_UNUSED) \ + _(2047, ZEND_ISSET_ISEMPTY_VAR_SPEC_TMP_UNUSED) \ + _(2050, ZEND_ISSET_ISEMPTY_VAR_SPEC_CV_UNUSED) \ + _(2051, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CONST) \ + _(2052, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_TMP) \ + _(2055, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CV) \ + _(2056, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_CONST) \ + _(2057, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_TMP) \ + _(2060, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_CV) \ + _(2071, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CONST) \ + _(2072, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_TMP) \ + _(2075, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CV) \ + _(2076, ZEND_SEND_VAL_EX_SPEC_CONST_CONST) \ + _(2077, ZEND_SEND_VAL_EX_SPEC_CONST_CONST) \ + _(2082, ZEND_SEND_VAL_EX_SPEC_CONST_UNUSED) \ + _(2083, ZEND_SEND_VAL_EX_SPEC_CONST_UNUSED_QUICK) \ + _(2086, ZEND_SEND_VAL_EX_SPEC_TMP_CONST) \ + _(2087, ZEND_SEND_VAL_EX_SPEC_TMP_CONST) \ + _(2092, ZEND_SEND_VAL_EX_SPEC_TMP_UNUSED) \ + _(2093, ZEND_SEND_VAL_EX_SPEC_TMP_UNUSED_QUICK) \ + _(2136, ZEND_SEND_VAR_SPEC_VAR_CONST) \ + _(2139, ZEND_SEND_VAR_SPEC_VAR_UNUSED) \ + _(2146, ZEND_SEND_VAR_SPEC_CV_CONST) \ + _(2149, ZEND_SEND_VAR_SPEC_CV_UNUSED) \ + _(2151, ZEND_INIT_USER_CALL_SPEC_CONST_CONST) \ + _(2152, ZEND_INIT_USER_CALL_SPEC_CONST_TMP) \ + _(2155, ZEND_INIT_USER_CALL_SPEC_CONST_CV) \ + _(2156, ZEND_SEND_ARRAY_SPEC) \ + _(2157, ZEND_SEND_USER_SPEC_CONST) \ + _(2158, ZEND_SEND_USER_SPEC_TMP) \ + _(2161, ZEND_SEND_USER_SPEC_CV) \ + _(2162, ZEND_STRLEN_SPEC_CONST) \ + _(2163, ZEND_STRLEN_SPEC_TMP) \ + _(2166, ZEND_STRLEN_SPEC_CV) \ + _(2167, ZEND_DEFINED_SPEC_CONST) \ + _(2168, ZEND_TYPE_CHECK_SPEC_CONST) \ + _(2169, ZEND_TYPE_CHECK_SPEC_TMP) \ + _(2172, ZEND_TYPE_CHECK_SPEC_CV) \ + _(2173, ZEND_VERIFY_RETURN_TYPE_SPEC_CONST_UNUSED) \ + _(2174, ZEND_VERIFY_RETURN_TYPE_SPEC_TMP_UNUSED) \ + _(2175, ZEND_VERIFY_RETURN_TYPE_SPEC_VAR_UNUSED) \ + _(2176, ZEND_VERIFY_RETURN_TYPE_SPEC_UNUSED_UNUSED) \ + _(2177, ZEND_VERIFY_RETURN_TYPE_SPEC_CV_UNUSED) \ + _(2178, ZEND_FE_RESET_RW_SPEC_CONST) \ + _(2179, ZEND_FE_RESET_RW_SPEC_TMP) \ + _(2180, ZEND_FE_RESET_RW_SPEC_VAR) \ + _(2182, ZEND_FE_RESET_RW_SPEC_CV) \ + _(2183, ZEND_FE_FETCH_RW_SPEC_VAR) \ + _(2184, ZEND_FE_FREE_SPEC_TMPVAR) \ + _(2185, ZEND_INIT_DYNAMIC_CALL_SPEC_CONST) \ + _(2186, ZEND_INIT_DYNAMIC_CALL_SPEC_TMP) \ + _(2189, ZEND_INIT_DYNAMIC_CALL_SPEC_CV) \ + _(2190, ZEND_DO_ICALL_SPEC_RETVAL_UNUSED) \ + _(2191, ZEND_DO_ICALL_SPEC_RETVAL_USED) \ + _(2192, ZEND_DO_ICALL_SPEC_OBSERVER) \ + _(2193, ZEND_DO_ICALL_SPEC_OBSERVER) \ + _(2194, ZEND_DO_UCALL_SPEC_RETVAL_UNUSED) \ + _(2195, ZEND_DO_UCALL_SPEC_RETVAL_USED) \ + _(2196, ZEND_DO_UCALL_SPEC_OBSERVER) \ + _(2197, ZEND_DO_UCALL_SPEC_OBSERVER) \ + _(2198, ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_UNUSED) \ + _(2199, ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED) \ + _(2200, ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER) \ + _(2201, ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER) \ + _(2212, ZEND_PRE_INC_OBJ_SPEC_VAR_CONST) \ + _(2213, ZEND_PRE_INC_OBJ_SPEC_VAR_TMP) \ + _(2216, ZEND_PRE_INC_OBJ_SPEC_VAR_CV) \ + _(2217, ZEND_PRE_INC_OBJ_SPEC_UNUSED_CONST) \ + _(2218, ZEND_PRE_INC_OBJ_SPEC_UNUSED_TMP) \ + _(2221, ZEND_PRE_INC_OBJ_SPEC_UNUSED_CV) \ + _(2222, ZEND_PRE_INC_OBJ_SPEC_CV_CONST) \ + _(2223, ZEND_PRE_INC_OBJ_SPEC_CV_TMP) \ + _(2226, ZEND_PRE_INC_OBJ_SPEC_CV_CV) \ + _(2237, ZEND_POST_INC_OBJ_SPEC_VAR_CONST) \ + _(2238, ZEND_POST_INC_OBJ_SPEC_VAR_TMP) \ + _(2241, ZEND_POST_INC_OBJ_SPEC_VAR_CV) \ + _(2242, ZEND_POST_INC_OBJ_SPEC_UNUSED_CONST) \ + _(2243, ZEND_POST_INC_OBJ_SPEC_UNUSED_TMP) \ + _(2246, ZEND_POST_INC_OBJ_SPEC_UNUSED_CV) \ + _(2247, ZEND_POST_INC_OBJ_SPEC_CV_CONST) \ + _(2248, ZEND_POST_INC_OBJ_SPEC_CV_TMP) \ + _(2251, ZEND_POST_INC_OBJ_SPEC_CV_CV) \ + _(2252, ZEND_ECHO_SPEC_CONST) \ + _(2253, ZEND_ECHO_SPEC_TMP) \ + _(2256, ZEND_ECHO_SPEC_CV) \ + _(2263, ZEND_INSTANCEOF_SPEC_TMP_CONST) \ + _(2265, ZEND_INSTANCEOF_SPEC_TMP_VAR) \ + _(2266, ZEND_INSTANCEOF_SPEC_TMP_UNUSED) \ + _(2278, ZEND_INSTANCEOF_SPEC_CV_CONST) \ + _(2280, ZEND_INSTANCEOF_SPEC_CV_VAR) \ + _(2281, ZEND_INSTANCEOF_SPEC_CV_UNUSED) \ + _(2283, ZEND_GENERATOR_CREATE_SPEC) \ + _(2286, ZEND_MAKE_REF_SPEC_VAR_UNUSED) \ + _(2288, ZEND_MAKE_REF_SPEC_CV_UNUSED) \ + _(2289, ZEND_DECLARE_FUNCTION_SPEC) \ + _(2290, ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST) \ + _(2291, ZEND_DECLARE_CONST_SPEC_CONST_CONST) \ + _(2292, ZEND_DECLARE_CLASS_SPEC_CONST) \ + _(2293, ZEND_DECLARE_CLASS_DELAYED_SPEC_CONST_CONST) \ + _(2294, ZEND_DECLARE_ANON_CLASS_SPEC) \ + _(2295, ZEND_ADD_ARRAY_UNPACK_SPEC) \ + _(2296, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_CONST) \ + _(2297, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_TMP) \ + _(2300, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_CV) \ + _(2301, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_CONST) \ + _(2302, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_TMP) \ + _(2305, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_CV) \ + _(2311, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_CONST) \ + _(2312, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_TMP) \ + _(2315, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_CV) \ + _(2316, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CONST) \ + _(2317, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_TMP) \ + _(2320, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CV) \ + _(2321, ZEND_HANDLE_EXCEPTION_SPEC) \ + _(2322, ZEND_USER_OPCODE_SPEC) \ + _(2323, ZEND_ASSERT_CHECK_SPEC) \ + _(2324, ZEND_JMP_SET_SPEC_CONST) \ + _(2325, ZEND_JMP_SET_SPEC_TMP) \ + _(2328, ZEND_JMP_SET_SPEC_CV) \ + _(2329, ZEND_UNSET_CV_SPEC_CV_UNUSED) \ + _(2330, ZEND_ISSET_ISEMPTY_CV_SPEC_CV_UNUSED_SET) \ + _(2331, ZEND_ISSET_ISEMPTY_CV_SPEC_CV_UNUSED_EMPTY) \ + _(2332, ZEND_FETCH_LIST_W_SPEC_VAR_CONST) \ + _(2333, ZEND_FETCH_LIST_W_SPEC_VAR_TMP) \ + _(2336, ZEND_FETCH_LIST_W_SPEC_VAR_CV) \ + _(2337, ZEND_SEPARATE_SPEC_VAR_UNUSED) \ + _(2339, ZEND_FETCH_CLASS_NAME_SPEC_TMP) \ + _(2341, ZEND_FETCH_CLASS_NAME_SPEC_UNUSED) \ + _(2342, ZEND_FETCH_CLASS_NAME_SPEC_CV) \ + _(2343, ZEND_CALL_TRAMPOLINE_SPEC) \ + _(2344, ZEND_CALL_TRAMPOLINE_SPEC_OBSERVER) \ + _(2345, ZEND_DISCARD_EXCEPTION_SPEC) \ + _(2346, ZEND_YIELD_SPEC_CONST_CONST) \ + _(2347, ZEND_YIELD_SPEC_CONST_TMP) \ + _(2349, ZEND_YIELD_SPEC_CONST_UNUSED) \ + _(2350, ZEND_YIELD_SPEC_CONST_CV) \ + _(2351, ZEND_YIELD_SPEC_TMP_CONST) \ + _(2352, ZEND_YIELD_SPEC_TMP_TMP) \ + _(2354, ZEND_YIELD_SPEC_TMP_UNUSED) \ + _(2355, ZEND_YIELD_SPEC_TMP_CV) \ + _(2356, ZEND_YIELD_SPEC_VAR_CONST) \ + _(2357, ZEND_YIELD_SPEC_VAR_TMP) \ + _(2359, ZEND_YIELD_SPEC_VAR_UNUSED) \ + _(2360, ZEND_YIELD_SPEC_VAR_CV) \ + _(2361, ZEND_YIELD_SPEC_UNUSED_CONST) \ + _(2362, ZEND_YIELD_SPEC_UNUSED_TMP) \ + _(2364, ZEND_YIELD_SPEC_UNUSED_UNUSED) \ + _(2365, ZEND_YIELD_SPEC_UNUSED_CV) \ + _(2366, ZEND_YIELD_SPEC_CV_CONST) \ + _(2367, ZEND_YIELD_SPEC_CV_TMP) \ + _(2369, ZEND_YIELD_SPEC_CV_UNUSED) \ + _(2370, ZEND_YIELD_SPEC_CV_CV) \ + _(2371, ZEND_GENERATOR_RETURN_SPEC_CONST) \ _(2372, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ - _(2375, ZEND_GENERATOR_RETURN_SPEC_CV) \ + _(2373, ZEND_GENERATOR_RETURN_SPEC_TMP) \ + _(2374, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ + _(2375, ZEND_GENERATOR_RETURN_SPEC_VAR) \ _(2376, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ - _(2377, ZEND_FAST_CALL_SPEC) \ - _(2378, ZEND_FAST_RET_SPEC) \ - _(2379, ZEND_RECV_VARIADIC_SPEC_UNUSED) \ - _(2380, ZEND_SEND_UNPACK_SPEC) \ - _(2381, ZEND_YIELD_FROM_SPEC_CONST) \ - _(2382, ZEND_YIELD_FROM_SPEC_TMP) \ - _(2385, ZEND_YIELD_FROM_SPEC_CV) \ - _(2386, ZEND_COPY_TMP_SPEC_TMPVAR_UNUSED) \ - _(2387, ZEND_BIND_GLOBAL_SPEC_CV_CONST) \ - _(2388, ZEND_COALESCE_SPEC_CONST) \ - _(2389, ZEND_COALESCE_SPEC_TMP) \ - _(2392, ZEND_COALESCE_SPEC_CV) \ - _(2393, ZEND_SPACESHIP_SPEC_CONST_CONST) \ - _(2394, ZEND_SPACESHIP_SPEC_CONST_TMP) \ - _(2397, ZEND_SPACESHIP_SPEC_CONST_CV) \ - _(2398, ZEND_SPACESHIP_SPEC_TMP_CONST) \ - _(2399, ZEND_SPACESHIP_SPEC_TMP_TMP) \ - _(2402, ZEND_SPACESHIP_SPEC_TMP_CV) \ - _(2413, ZEND_SPACESHIP_SPEC_CV_CONST) \ - _(2414, ZEND_SPACESHIP_SPEC_CV_TMP) \ - _(2417, ZEND_SPACESHIP_SPEC_CV_CV) \ - _(2418, ZEND_FUNC_NUM_ARGS_SPEC_UNUSED_UNUSED) \ - _(2419, ZEND_FUNC_GET_ARGS_SPEC_CONST_UNUSED) \ - _(2422, ZEND_FUNC_GET_ARGS_SPEC_UNUSED_UNUSED) \ - _(2424, ZEND_FETCH_STATIC_PROP_R_SPEC) \ - _(2425, ZEND_FETCH_STATIC_PROP_W_SPEC) \ - _(2426, ZEND_FETCH_STATIC_PROP_RW_SPEC) \ - _(2427, ZEND_FETCH_STATIC_PROP_IS_SPEC) \ - _(2428, ZEND_FETCH_STATIC_PROP_FUNC_ARG_SPEC) \ - _(2429, ZEND_FETCH_STATIC_PROP_UNSET_SPEC) \ - _(2430, ZEND_UNSET_STATIC_PROP_SPEC) \ - _(2431, ZEND_ISSET_ISEMPTY_STATIC_PROP_SPEC) \ - _(2432, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_CONST) \ - _(2433, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ - _(2434, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ - _(2436, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ - _(2442, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_CONST) \ - _(2443, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ - _(2444, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ - _(2446, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ - _(2447, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_CONST) \ - _(2448, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ - _(2449, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ - _(2451, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ - _(2457, ZEND_BIND_LEXICAL_SPEC_TMP_CV) \ - _(2458, ZEND_BIND_STATIC_SPEC_CV) \ - _(2459, ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED) \ - _(2460, ZEND_SEND_FUNC_ARG_SPEC_VAR_CONST) \ - _(2463, ZEND_SEND_FUNC_ARG_SPEC_VAR_UNUSED) \ - _(2465, ZEND_ISSET_ISEMPTY_THIS_SPEC_UNUSED_UNUSED) \ - _(2466, ZEND_SWITCH_LONG_SPEC_CONST_CONST) \ - _(2467, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ - _(2468, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ - _(2470, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ - _(2471, ZEND_SWITCH_STRING_SPEC_CONST_CONST) \ - _(2472, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ - _(2473, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ - _(2475, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ - _(2476, ZEND_IN_ARRAY_SPEC_CONST_CONST) \ - _(2477, ZEND_IN_ARRAY_SPEC_TMP_CONST) \ - _(2480, ZEND_IN_ARRAY_SPEC_CV_CONST) \ - _(2481, ZEND_COUNT_SPEC_CONST_UNUSED) \ - _(2482, ZEND_COUNT_SPEC_TMP_UNUSED) \ - _(2485, ZEND_COUNT_SPEC_CV_UNUSED) \ - _(2486, ZEND_GET_CLASS_SPEC_CONST_UNUSED) \ - _(2487, ZEND_GET_CLASS_SPEC_TMP_UNUSED) \ - _(2489, ZEND_GET_CLASS_SPEC_UNUSED_UNUSED) \ - _(2490, ZEND_GET_CLASS_SPEC_CV_UNUSED) \ - _(2491, ZEND_GET_CALLED_CLASS_SPEC_UNUSED_UNUSED) \ - _(2492, ZEND_GET_TYPE_SPEC_CONST_UNUSED) \ - _(2493, ZEND_GET_TYPE_SPEC_TMP_UNUSED) \ - _(2496, ZEND_GET_TYPE_SPEC_CV_UNUSED) \ - _(2497, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_CONST) \ - _(2498, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_TMP) \ - _(2501, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_CV) \ - _(2502, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_CONST) \ - _(2503, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_TMP) \ - _(2506, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_CV) \ - _(2517, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_CONST) \ - _(2518, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_TMP) \ - _(2521, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_CV) \ - _(2522, ZEND_MATCH_SPEC_CONST_CONST) \ - _(2523, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ - _(2524, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ - _(2526, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ - _(2527, ZEND_CASE_STRICT_SPEC_TMP_CONST) \ - _(2528, ZEND_CASE_STRICT_SPEC_TMP_TMP) \ - _(2531, ZEND_CASE_STRICT_SPEC_TMP_CV) \ - _(2532, ZEND_MATCH_ERROR_SPEC_CONST_UNUSED) \ - _(2533, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ - _(2534, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ - _(2536, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ - _(2537, ZEND_JMP_NULL_SPEC_CONST) \ - _(2538, ZEND_JMP_NULL_SPEC_TMP) \ - _(2541, ZEND_JMP_NULL_SPEC_CV) \ - _(2542, ZEND_CHECK_UNDEF_ARGS_SPEC_UNUSED_UNUSED) \ - _(2543, ZEND_FETCH_GLOBALS_SPEC_UNUSED_UNUSED) \ - _(2544, ZEND_VERIFY_NEVER_TYPE_SPEC_UNUSED_UNUSED) \ - _(2545, ZEND_CALLABLE_CONVERT_SPEC_UNUSED_UNUSED) \ - _(2546, ZEND_BIND_INIT_STATIC_OR_JMP_SPEC_CV) \ - _(2547, ZEND_FRAMELESS_ICALL_0_SPEC_UNUSED_UNUSED) \ - _(2548, ZEND_FRAMELESS_ICALL_0_SPEC_OBSERVER) \ - _(2549, ZEND_FRAMELESS_ICALL_1_SPEC_UNUSED) \ - _(2550, ZEND_FRAMELESS_ICALL_1_SPEC_OBSERVER) \ - _(2551, ZEND_FRAMELESS_ICALL_2_SPEC) \ - _(2552, ZEND_FRAMELESS_ICALL_2_SPEC_OBSERVER) \ - _(2553, ZEND_FRAMELESS_ICALL_3_SPEC) \ - _(2554, ZEND_FRAMELESS_ICALL_3_SPEC_OBSERVER) \ - _(2555, ZEND_JMP_FRAMELESS_SPEC_CONST) \ - _(2556, ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED) \ - _(2557, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST) \ - _(2558, ZEND_TYPE_ASSERT_SPEC_CONST) \ - _(2560, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) \ - _(2562, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) \ - _(2564, ZEND_INIT_FCALL_OFFSET_SPEC_CONST) \ - _(2565, ZEND_RECV_NOTYPE_SPEC) \ - _(2567, ZEND_COUNT_ARRAY_SPEC_TMP_UNUSED) \ - _(2570, ZEND_COUNT_ARRAY_SPEC_CV_UNUSED) \ - _(2571, ZEND_JMP_FORWARD_SPEC) \ - _(2577, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2578, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2579, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2581, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2582, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2379, ZEND_GENERATOR_RETURN_SPEC_CV) \ + _(2380, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ + _(2381, ZEND_FAST_CALL_SPEC) \ + _(2382, ZEND_FAST_RET_SPEC) \ + _(2383, ZEND_RECV_VARIADIC_SPEC_UNUSED) \ + _(2384, ZEND_SEND_UNPACK_SPEC) \ + _(2385, ZEND_YIELD_FROM_SPEC_CONST) \ + _(2386, ZEND_YIELD_FROM_SPEC_TMP) \ + _(2389, ZEND_YIELD_FROM_SPEC_CV) \ + _(2390, ZEND_COPY_TMP_SPEC_TMPVAR_UNUSED) \ + _(2391, ZEND_BIND_GLOBAL_SPEC_CV_CONST) \ + _(2392, ZEND_COALESCE_SPEC_CONST) \ + _(2393, ZEND_COALESCE_SPEC_TMP) \ + _(2396, ZEND_COALESCE_SPEC_CV) \ + _(2397, ZEND_SPACESHIP_SPEC_CONST_CONST) \ + _(2398, ZEND_SPACESHIP_SPEC_CONST_TMP) \ + _(2401, ZEND_SPACESHIP_SPEC_CONST_CV) \ + _(2402, ZEND_SPACESHIP_SPEC_TMP_CONST) \ + _(2403, ZEND_SPACESHIP_SPEC_TMP_TMP) \ + _(2406, ZEND_SPACESHIP_SPEC_TMP_CV) \ + _(2417, ZEND_SPACESHIP_SPEC_CV_CONST) \ + _(2418, ZEND_SPACESHIP_SPEC_CV_TMP) \ + _(2421, ZEND_SPACESHIP_SPEC_CV_CV) \ + _(2422, ZEND_FUNC_NUM_ARGS_SPEC_UNUSED_UNUSED) \ + _(2423, ZEND_FUNC_GET_ARGS_SPEC_CONST_UNUSED) \ + _(2426, ZEND_FUNC_GET_ARGS_SPEC_UNUSED_UNUSED) \ + _(2428, ZEND_FETCH_STATIC_PROP_R_SPEC) \ + _(2429, ZEND_FETCH_STATIC_PROP_W_SPEC) \ + _(2430, ZEND_FETCH_STATIC_PROP_RW_SPEC) \ + _(2431, ZEND_FETCH_STATIC_PROP_IS_SPEC) \ + _(2432, ZEND_FETCH_STATIC_PROP_FUNC_ARG_SPEC) \ + _(2433, ZEND_FETCH_STATIC_PROP_UNSET_SPEC) \ + _(2434, ZEND_UNSET_STATIC_PROP_SPEC) \ + _(2435, ZEND_ISSET_ISEMPTY_STATIC_PROP_SPEC) \ + _(2436, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_CONST) \ + _(2437, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ + _(2438, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ + _(2440, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ + _(2446, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_CONST) \ + _(2447, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ + _(2448, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ + _(2450, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ + _(2451, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_CONST) \ + _(2452, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ + _(2453, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ + _(2455, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ + _(2461, ZEND_BIND_LEXICAL_SPEC_TMP_CV) \ + _(2462, ZEND_BIND_STATIC_SPEC_CV) \ + _(2463, ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED) \ + _(2464, ZEND_SEND_FUNC_ARG_SPEC_VAR_CONST) \ + _(2467, ZEND_SEND_FUNC_ARG_SPEC_VAR_UNUSED) \ + _(2469, ZEND_ISSET_ISEMPTY_THIS_SPEC_UNUSED_UNUSED) \ + _(2470, ZEND_SWITCH_LONG_SPEC_CONST_CONST) \ + _(2471, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ + _(2472, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ + _(2474, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ + _(2475, ZEND_SWITCH_STRING_SPEC_CONST_CONST) \ + _(2476, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ + _(2477, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ + _(2479, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ + _(2480, ZEND_IN_ARRAY_SPEC_CONST_CONST) \ + _(2481, ZEND_IN_ARRAY_SPEC_TMP_CONST) \ + _(2484, ZEND_IN_ARRAY_SPEC_CV_CONST) \ + _(2485, ZEND_COUNT_SPEC_CONST_UNUSED) \ + _(2486, ZEND_COUNT_SPEC_TMP_UNUSED) \ + _(2489, ZEND_COUNT_SPEC_CV_UNUSED) \ + _(2490, ZEND_GET_CLASS_SPEC_CONST_UNUSED) \ + _(2491, ZEND_GET_CLASS_SPEC_TMP_UNUSED) \ + _(2493, ZEND_GET_CLASS_SPEC_UNUSED_UNUSED) \ + _(2494, ZEND_GET_CLASS_SPEC_CV_UNUSED) \ + _(2495, ZEND_GET_CALLED_CLASS_SPEC_UNUSED_UNUSED) \ + _(2496, ZEND_GET_TYPE_SPEC_CONST_UNUSED) \ + _(2497, ZEND_GET_TYPE_SPEC_TMP_UNUSED) \ + _(2500, ZEND_GET_TYPE_SPEC_CV_UNUSED) \ + _(2501, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_CONST) \ + _(2502, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_TMP) \ + _(2505, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_CV) \ + _(2506, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_CONST) \ + _(2507, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_TMP) \ + _(2510, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_CV) \ + _(2521, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_CONST) \ + _(2522, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_TMP) \ + _(2525, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_CV) \ + _(2526, ZEND_MATCH_SPEC_CONST_CONST) \ + _(2527, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ + _(2528, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ + _(2530, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ + _(2531, ZEND_CASE_STRICT_SPEC_TMP_CONST) \ + _(2532, ZEND_CASE_STRICT_SPEC_TMP_TMP) \ + _(2535, ZEND_CASE_STRICT_SPEC_TMP_CV) \ + _(2536, ZEND_MATCH_ERROR_SPEC_CONST_UNUSED) \ + _(2537, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ + _(2538, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ + _(2540, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ + _(2541, ZEND_JMP_NULL_SPEC_CONST) \ + _(2542, ZEND_JMP_NULL_SPEC_TMP) \ + _(2545, ZEND_JMP_NULL_SPEC_CV) \ + _(2546, ZEND_CHECK_UNDEF_ARGS_SPEC_UNUSED_UNUSED) \ + _(2547, ZEND_FETCH_GLOBALS_SPEC_UNUSED_UNUSED) \ + _(2548, ZEND_VERIFY_NEVER_TYPE_SPEC_UNUSED_UNUSED) \ + _(2549, ZEND_CALLABLE_CONVERT_SPEC_UNUSED_UNUSED) \ + _(2550, ZEND_BIND_INIT_STATIC_OR_JMP_SPEC_CV) \ + _(2551, ZEND_FRAMELESS_ICALL_0_SPEC_UNUSED_UNUSED) \ + _(2552, ZEND_FRAMELESS_ICALL_0_SPEC_OBSERVER) \ + _(2553, ZEND_FRAMELESS_ICALL_1_SPEC_UNUSED) \ + _(2554, ZEND_FRAMELESS_ICALL_1_SPEC_OBSERVER) \ + _(2555, ZEND_FRAMELESS_ICALL_2_SPEC) \ + _(2556, ZEND_FRAMELESS_ICALL_2_SPEC_OBSERVER) \ + _(2557, ZEND_FRAMELESS_ICALL_3_SPEC) \ + _(2558, ZEND_FRAMELESS_ICALL_3_SPEC_OBSERVER) \ + _(2559, ZEND_JMP_FRAMELESS_SPEC_CONST) \ + _(2560, ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED) \ + _(2561, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST) \ + _(2562, ZEND_TYPE_ASSERT_SPEC_CONST) \ + _(2564, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) \ + _(2566, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) \ + _(2568, ZEND_INIT_FCALL_OFFSET_SPEC_CONST) \ + _(2569, ZEND_RECV_NOTYPE_SPEC) \ + _(2571, ZEND_COUNT_ARRAY_SPEC_TMP_UNUSED) \ + _(2574, ZEND_COUNT_ARRAY_SPEC_CV_UNUSED) \ + _(2575, ZEND_JMP_FORWARD_SPEC) \ + _(2581, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2582, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2583, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2584, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2586, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2592, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2593, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2594, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2596, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2602, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ - _(2603, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2604, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2606, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2607, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2585, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2586, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2587, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2588, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2590, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2596, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2597, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2598, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2600, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2606, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2607, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2608, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2609, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2611, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2617, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ - _(2618, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2619, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2621, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2627, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2628, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2629, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2631, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2632, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2610, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2611, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2612, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2613, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2615, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2621, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2622, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2623, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2625, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2631, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2632, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2633, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2634, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2636, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2642, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2643, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2644, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2646, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2648, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2649, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2651, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2652, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2653, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2654, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2656, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2657, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2635, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2636, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2637, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2638, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2640, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2646, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2647, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2648, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2650, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2652, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2653, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2655, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2656, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2657, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2658, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2659, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2661, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2667, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2668, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2669, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2671, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2673, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2674, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2676, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2677, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ - _(2678, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2679, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2681, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2682, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2660, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2661, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2662, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2663, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2665, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2671, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2672, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2673, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2675, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2677, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2678, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2680, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2681, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2682, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2683, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2684, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2686, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2692, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ - _(2693, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2694, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2696, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2698, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2699, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2701, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2702, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2703, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2704, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2706, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2707, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2685, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2686, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2687, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2688, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2690, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2696, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2697, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2698, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2700, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2702, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2703, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2705, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2706, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2707, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2708, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2709, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2711, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2717, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2718, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2719, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2721, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2727, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2728, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2729, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2731, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2732, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2710, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2711, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2712, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2713, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2715, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2721, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2722, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2723, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2725, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2731, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2732, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2733, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2734, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2736, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2742, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2743, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2744, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2746, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2752, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ - _(2753, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2754, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2756, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2757, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2735, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2736, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2737, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2738, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2740, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2746, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2747, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2748, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2750, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2756, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2757, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2758, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2759, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2761, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2767, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ - _(2768, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2769, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2771, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2777, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2778, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2779, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2781, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2782, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2760, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2761, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2762, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2763, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2765, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2771, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2772, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2773, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2775, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2781, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2782, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2783, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2784, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2786, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2792, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2793, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2794, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2796, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2812, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2813, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2814, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2815, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2816, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2817, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2818, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2819, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2820, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2824, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2825, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2826, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2827, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2828, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2829, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2830, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2831, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2832, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2833, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2834, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2835, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2839, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2840, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2841, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2857, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2858, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2859, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2860, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2861, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2862, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2863, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2864, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2865, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2869, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2870, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2871, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2887, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2888, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2889, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2890, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2891, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2892, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2893, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2894, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2895, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2899, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2900, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2901, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2902, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2903, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2904, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2905, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2906, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2907, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2908, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2909, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2910, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2914, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2915, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2916, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2932, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2933, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2934, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2935, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2936, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2937, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2938, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2939, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2940, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2944, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2945, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2946, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2962, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2963, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2964, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2965, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2966, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2967, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2968, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2969, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2970, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2974, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2975, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2976, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2977, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2978, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2979, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2980, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2981, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2982, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2983, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2984, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2985, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2989, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2990, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2991, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3007, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3008, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3009, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3010, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3011, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3012, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3013, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3014, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3015, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3019, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3020, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3021, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3037, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3038, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3039, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3040, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3041, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3042, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3043, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3044, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3045, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3049, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3050, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3051, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3052, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3053, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3054, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3055, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3056, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3057, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3058, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3059, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3060, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3064, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3065, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3066, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3082, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3083, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3084, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3085, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3086, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3087, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3088, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3089, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3090, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3094, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3095, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3096, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3097, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ - _(3098, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3099, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3100, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ - _(3101, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3102, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3103, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ - _(3107, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CV) \ - _(3108, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ - _(3112, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CV) \ - _(3116, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3117, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3118, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3119, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3120, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3121, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3125, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3126, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3127, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3128, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3129, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3130, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3131, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3132, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3133, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3134, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3135, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3136, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3140, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3141, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3142, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3143, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3144, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3145, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3146, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3147, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3148, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3149, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3150, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3151, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3155, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3156, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3157, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3173, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3174, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3175, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3176, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3177, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3178, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3179, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3180, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3181, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3185, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3186, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3187, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3191, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3192, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3193, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3194, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3195, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3196, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3200, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3201, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3202, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3203, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3204, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3205, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3206, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3207, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3208, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3209, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3210, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3211, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3215, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3216, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3217, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3218, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3219, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3220, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3221, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3222, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3223, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3224, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3225, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3226, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3230, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3231, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3232, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3248, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3249, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3250, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3251, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3252, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3253, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3254, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3255, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3256, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3260, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3261, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3262, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3266, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3267, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3268, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3269, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3270, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3271, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3275, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3276, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3277, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3278, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3279, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3280, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3281, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3282, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3283, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3284, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3285, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3286, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3290, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3291, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3292, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3293, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3294, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3295, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3296, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3297, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3298, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3299, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3300, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3301, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3305, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3306, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3307, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3323, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3324, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3325, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3326, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3327, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3328, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3329, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3330, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3331, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3335, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3336, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3337, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3341, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3342, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3343, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3344, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3345, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3346, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3350, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3351, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3352, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3353, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3354, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3355, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3356, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3357, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3358, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3359, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3360, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3361, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3365, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3366, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3367, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3368, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3369, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3370, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3371, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3372, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3373, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3374, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3375, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3376, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3380, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3381, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3382, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3398, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3399, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3400, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3401, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3402, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3403, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3404, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3405, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3406, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3410, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3411, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3412, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3413, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ - _(3414, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ - _(3415, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_UNUSED) \ - _(3416, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_USED) \ - _(3417, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ - _(3418, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ - _(3419, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_UNUSED) \ - _(3420, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_USED) \ - _(3421, ZEND_POST_INC_LONG_NO_OVERFLOW_SPEC_CV) \ - _(3422, ZEND_POST_INC_LONG_SPEC_CV) \ - _(3423, ZEND_POST_DEC_LONG_NO_OVERFLOW_SPEC_CV) \ - _(3424, ZEND_POST_DEC_LONG_SPEC_CV) \ - _(3425, ZEND_QM_ASSIGN_LONG_SPEC_CONST) \ - _(3426, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3427, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3429, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3430, ZEND_QM_ASSIGN_DOUBLE_SPEC_CONST) \ - _(3431, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3432, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3434, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3435, ZEND_QM_ASSIGN_NOREF_SPEC_CONST) \ - _(3436, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3437, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3439, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3441, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3442, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3444, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3445, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ - _(3446, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3447, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3449, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3450, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ + _(2785, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2786, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2787, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2788, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2790, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2796, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2797, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2798, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2800, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2816, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2817, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2818, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2819, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2820, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2821, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2822, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2823, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2824, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2828, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2829, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2830, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2831, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2832, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2833, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2834, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2835, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2836, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2837, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2838, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2839, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2843, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2844, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2845, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2861, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2862, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2863, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2864, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2865, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2866, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2867, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2868, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2869, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2873, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2874, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2875, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2891, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2892, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2893, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2894, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2895, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2896, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2897, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2898, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2899, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2903, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2904, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2905, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2906, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2907, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2908, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2909, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2910, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2911, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2912, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2913, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2914, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2918, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2919, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2920, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2936, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2937, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2938, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2939, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2940, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2941, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2942, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2943, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2944, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2948, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2949, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2950, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2966, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2967, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2968, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2969, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2970, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2971, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2972, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2973, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2974, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2978, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2979, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2980, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2981, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2982, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2983, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2984, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2985, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2986, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2987, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2988, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2989, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2993, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2994, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2995, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3011, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3012, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3013, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3014, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3015, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3016, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3017, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3018, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3019, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3023, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3024, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3025, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3041, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3042, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3043, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3044, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3045, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3046, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3047, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3048, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3049, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3053, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3054, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3055, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3056, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3057, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3058, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3059, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3060, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3061, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3062, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3063, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3064, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3068, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3069, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3070, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3086, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3087, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3088, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3089, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3090, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3091, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3092, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3093, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3094, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3098, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3099, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3100, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3101, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ + _(3102, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3103, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3104, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ + _(3105, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3106, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3107, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ + _(3111, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CV) \ + _(3112, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ + _(3116, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CV) \ + _(3120, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3121, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3122, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3123, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3124, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3125, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3129, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3130, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3131, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3132, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3133, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3134, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3135, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3136, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3137, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3138, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3139, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3140, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3144, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3145, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3146, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3147, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3148, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3149, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3150, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3151, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3152, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3153, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3154, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3155, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3159, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3160, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3161, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3177, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3178, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3179, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3180, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3181, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3182, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3183, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3184, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3185, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3189, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3190, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3191, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3195, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3196, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3197, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3198, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3199, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3200, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3204, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3205, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3206, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3207, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3208, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3209, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3210, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3211, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3212, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3213, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3214, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3215, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3219, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3220, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3221, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3222, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3223, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3224, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3225, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3226, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3227, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3228, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3229, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3230, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3234, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3235, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3236, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3252, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3253, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3254, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3255, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3256, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3257, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3258, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3259, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3260, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3264, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3265, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3266, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3270, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3271, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3272, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3273, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3274, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3275, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3279, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3280, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3281, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3282, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3283, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3284, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3285, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3286, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3287, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3288, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3289, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3290, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3294, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3295, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3296, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3297, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3298, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3299, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3300, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3301, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3302, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3303, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3304, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3305, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3309, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3310, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3311, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3327, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3328, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3329, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3330, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3331, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3332, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3333, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3334, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3335, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3339, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3340, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3341, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3345, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3346, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3347, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3348, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3349, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3350, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3354, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3355, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3356, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3357, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3358, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3359, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3360, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3361, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3362, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3363, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3364, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3365, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3369, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3370, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3371, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3372, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3373, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3374, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3375, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3376, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3377, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3378, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3379, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3380, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3384, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3385, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3386, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3402, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3403, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3404, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3405, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3406, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3407, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3408, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3409, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3410, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3414, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3415, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3416, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3417, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ + _(3418, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ + _(3419, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_UNUSED) \ + _(3420, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_USED) \ + _(3421, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ + _(3422, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ + _(3423, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_UNUSED) \ + _(3424, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_USED) \ + _(3425, ZEND_POST_INC_LONG_NO_OVERFLOW_SPEC_CV) \ + _(3426, ZEND_POST_INC_LONG_SPEC_CV) \ + _(3427, ZEND_POST_DEC_LONG_NO_OVERFLOW_SPEC_CV) \ + _(3428, ZEND_POST_DEC_LONG_SPEC_CV) \ + _(3429, ZEND_QM_ASSIGN_LONG_SPEC_CONST) \ + _(3430, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3431, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3433, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3434, ZEND_QM_ASSIGN_DOUBLE_SPEC_CONST) \ + _(3435, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3436, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3438, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3439, ZEND_QM_ASSIGN_NOREF_SPEC_CONST) \ + _(3440, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3441, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3443, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3445, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3446, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3448, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3449, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ + _(3450, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ _(3451, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3452, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3454, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3460, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_CONST) \ - _(3461, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3462, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3464, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3467, ZEND_SEND_VAR_SIMPLE_SPEC_VAR) \ - _(3469, ZEND_SEND_VAR_SIMPLE_SPEC_CV) \ - _(3472, ZEND_SEND_VAR_EX_SIMPLE_SPEC_VAR_UNUSED) \ - _(3474, ZEND_SEND_VAR_EX_SIMPLE_SPEC_CV_UNUSED) \ - _(3475, ZEND_SEND_VAL_SIMPLE_SPEC_CONST) \ - _(3476, ZEND_SEND_VAL_EX_SIMPLE_SPEC_CONST) \ - _(3477, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_UNUSED) \ - _(3478, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) \ - _(3478+1, ZEND_NULL) + _(3453, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3454, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ + _(3455, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3456, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3458, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3464, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_CONST) \ + _(3465, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3466, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3468, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3471, ZEND_SEND_VAR_SIMPLE_SPEC_VAR) \ + _(3473, ZEND_SEND_VAR_SIMPLE_SPEC_CV) \ + _(3476, ZEND_SEND_VAR_EX_SIMPLE_SPEC_VAR_UNUSED) \ + _(3478, ZEND_SEND_VAR_EX_SIMPLE_SPEC_CV_UNUSED) \ + _(3479, ZEND_SEND_VAL_SIMPLE_SPEC_CONST) \ + _(3480, ZEND_SEND_VAL_EX_SIMPLE_SPEC_CONST) \ + _(3481, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_UNUSED) \ + _(3482, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) \ + _(3482+1, ZEND_NULL) From 056dce318619a380a486c62c9439b9c1334db986 Mon Sep 17 00:00:00 2001 From: Rob Landers Date: Tue, 19 May 2026 19:06:08 +0200 Subject: [PATCH 06/30] add benchmarks --- bench/bench_reification.php | 300 ++++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 bench/bench_reification.php diff --git a/bench/bench_reification.php b/bench/bench_reification.php new file mode 100644 index 000000000000..9d25f66d8e49 --- /dev/null +++ b/bench/bench_reification.php @@ -0,0 +1,300 @@ + 0 ? $a_ns / $b_ns : 0; + printf(" %-55s %+9.1f ns/op delta (%.2fx %s vs %s)\n", + $label, $delta, $ratio, $a_name, $b_name); +} + +// -------------------------------------------------------------------------- +// Scenario 1: non-generic baseline — instantiation and method dispatch +// -------------------------------------------------------------------------- +class PlainFoo { public int $x = 0; } +class PlainBox { + public function __construct(public mixed $value = null) {} + public function tick(int $x): int { return $x + 1; } +} + +function plain_new(int $n): void { + for ($i = 0; $i < $n; $i++) { + new PlainFoo(); + } +} +function plain_method(int $n): void { + $b = new PlainBox(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = $b->tick($acc); + } +} + +// -------------------------------------------------------------------------- +// Scenario 2: generic-class instantiation +// -------------------------------------------------------------------------- +class GenBox { + public function __construct(public mixed $value = null) {} + public function tick(int $x): int { return $x + 1; } +} +class IntBox extends GenBox {} + +function gen_new_turbofish(int $n): void { + for ($i = 0; $i < $n; $i++) { + new GenBox::(); + } +} +function gen_new_default(int $n): void { + for ($i = 0; $i < $n; $i++) { + new GenBox(); + } +} +function gen_new_extends(int $n): void { + for ($i = 0; $i < $n; $i++) { + new IntBox(); + } +} + +// -------------------------------------------------------------------------- +// Scenario 3: method dispatch on generic-class instance +// -------------------------------------------------------------------------- +function gen_method_on_mono(int $n): void { + $b = new GenBox::(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = $b->tick($acc); + } +} +function gen_method_on_default(int $n): void { + $b = new GenBox(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = $b->tick($acc); + } +} + +// -------------------------------------------------------------------------- +// Scenario 4: T-keyed expressions inside the body +// -------------------------------------------------------------------------- +class Marker {} +class TBox { + public function makeT(): object { return new T(); } + public function classT(): string { return T::class; } + public function isT(object $x): bool { return $x instanceof T; } +} +function tbox_makeT(int $n): void { + $b = new TBox::(); + for ($i = 0; $i < $n; $i++) { + $b->makeT(); + } +} +function tbox_classT(int $n): void { + $b = new TBox::(); + for ($i = 0; $i < $n; $i++) { + $b->classT(); + } +} +function tbox_isT(int $n): void { + $b = new TBox::(); + $m = new Marker(); + for ($i = 0; $i < $n; $i++) { + $b->isT($m); + } +} +function plain_makeT(int $n): void { + for ($i = 0; $i < $n; $i++) { + new Marker(); + } +} +function plain_classT(int $n): void { + for ($i = 0; $i < $n; $i++) { + $_ = Marker::class; + } +} +function plain_isT(int $n): void { + $m = new Marker(); + for ($i = 0; $i < $n; $i++) { + $_ = $m instanceof Marker; + } +} + +// -------------------------------------------------------------------------- +// Scenario 5: inference vs turbofish vs non-generic +// -------------------------------------------------------------------------- +class Foo { public int $tag = 0; } +function makeGen(T $x): T { return $x; } +function makeNonGen(Foo $x): Foo { return $x; } + +function infer_call(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeGen($f); + } +} +function turbofish_call(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeGen::($f); + } +} +function plain_call(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeNonGen($f); + } +} + +// -------------------------------------------------------------------------- +// Scenario 6: multi-parameter inference +// -------------------------------------------------------------------------- +class Bar { public int $tag = 0; } +function makeTwoGen(T $x, U $y): T { return $x; } +function makeTwoNonGen(Foo $x, Bar $y): Foo { return $x; } + +function infer_two(int $n): void { + $f = new Foo(); + $b = new Bar(); + for ($i = 0; $i < $n; $i++) { + makeTwoGen($f, $b); + } +} +function turbofish_two(int $n): void { + $f = new Foo(); + $b = new Bar(); + for ($i = 0; $i < $n; $i++) { + makeTwoGen::($f, $b); + } +} +function plain_two(int $n): void { + $f = new Foo(); + $b = new Bar(); + for ($i = 0; $i < $n; $i++) { + makeTwoNonGen($f, $b); + } +} + +// -------------------------------------------------------------------------- +// Scenario 7: inference + bound conformance check +// -------------------------------------------------------------------------- +function makeBounded(T $x): T { return $x; } + +function infer_bounded(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeBounded($f); + } +} +function turbofish_bounded(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeBounded::($f); + } +} + +// -------------------------------------------------------------------------- +// Scenario 8: reified arg-type coercion (callee has T $x and turbofish) +// -------------------------------------------------------------------------- +function takesT(T $x): T { return $x; } +function takesTBounded(T $x): T { return $x; } +function takesPlain(Foo $x): Foo { return $x; } + +function reified_arg_check(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + takesTBounded::($f); + } +} +function plain_arg_check(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + takesPlain($f); + } +} + +// -------------------------------------------------------------------------- +// Driver +// -------------------------------------------------------------------------- +echo "PHP ", PHP_VERSION, " (", PHP_BINARY, ")\n"; +echo "Reified bound-erased generics benchmark\n"; + +section("1. Non-generic baseline"); +$plain_new_ns = bench("plain new Foo()", 'plain_new'); +$plain_method_ns = bench("plain \$box->tick()", 'plain_method'); + +section("2. Generic-class instantiation"); +$gen_tf_ns = bench("new GenBox::()", 'gen_new_turbofish'); +$gen_def_ns = bench("new GenBox() (default T=int)", 'gen_new_default'); +$gen_ext_ns = bench("new IntBox() (extends GenBox)", 'gen_new_extends'); +compare("delta: GenBox vs PlainFoo", $gen_tf_ns, "gen", $plain_new_ns, "plain"); +compare("delta: GenBox (default) vs PlainFoo", $gen_def_ns, "gen", $plain_new_ns, "plain"); +compare("delta: IntBox extends vs PlainFoo", $gen_ext_ns, "gen", $plain_new_ns, "plain"); + +section("3. Method dispatch on generic-class receiver"); +$gen_mono_method_ns = bench("GenBox->tick()", 'gen_method_on_mono'); +$gen_default_method_ns = bench("GenBox(default)->tick()", 'gen_method_on_default'); +compare("delta: GenBox->tick vs PlainBox->tick", $gen_mono_method_ns, "gen", $plain_method_ns, "plain"); + +section("4. T-keyed body expressions"); +$tbox_makeT_ns = bench("TBox->makeT()", 'tbox_makeT'); +$plain_makeT_ns = bench("new Marker() direct", 'plain_makeT'); +compare("delta: new T() vs new Marker()", $tbox_makeT_ns, "T", $plain_makeT_ns, "direct"); +$tbox_classT_ns = bench("TBox->classT()", 'tbox_classT'); +$plain_classT_ns = bench("Marker::class direct", 'plain_classT'); +compare("delta: T::class vs Marker::class", $tbox_classT_ns, "T", $plain_classT_ns, "direct"); +$tbox_isT_ns = bench("TBox->isT()", 'tbox_isT'); +$plain_isT_ns = bench("\$x instanceof Marker direct", 'plain_isT'); +compare("delta: instanceof T vs instanceof Marker", $tbox_isT_ns, "T", $plain_isT_ns, "direct"); + +section("5. Inference vs turbofish vs non-generic call"); +$infer_ns = bench("makeGen(\$x) — inference", 'infer_call'); +$turbo_ns = bench("makeGen::(\$x) — turbofish", 'turbofish_call'); +$plain_fn_ns = bench("makeNonGen(\$x) — no generics", 'plain_call'); +compare("delta: inference vs plain", $infer_ns, "infer", $plain_fn_ns, "plain"); +compare("delta: turbofish vs plain", $turbo_ns, "turbo", $plain_fn_ns, "plain"); +compare("delta: inference vs turbofish", $infer_ns, "infer", $turbo_ns, "turbo"); + +section("6. Multi-parameter inference"); +$infer2_ns = bench("makeTwoGen(\$f,\$b) — infer T,U", 'infer_two'); +$turbo2_ns = bench("makeTwoGen:: — turbofish", 'turbofish_two'); +$plain2_ns = bench("makeTwoNonGen(\$f,\$b)", 'plain_two'); +compare("delta: infer T,U vs plain", $infer2_ns, "infer2", $plain2_ns, "plain"); +compare("delta: turbofish T,U vs plain", $turbo2_ns, "turbo2", $plain2_ns, "plain"); + +section("7. Inference + bound check"); +$infer_bd_ns = bench("makeBounded(\$x) — infer + bound", 'infer_bounded'); +$turbo_bd_ns = bench("makeBounded::(\$x) — turbo+bd", 'turbofish_bounded'); +compare("delta: infer+bound vs infer (no bound)", $infer_bd_ns, "ibnd", $infer_ns, "infer"); +compare("delta: turbo+bound vs turbo (no bound)", $turbo_bd_ns, "tbnd", $turbo_ns, "turbo"); + +section("8. Reified arg-type coercion"); +$reified_arg_ns = bench("takesTBounded::(\$f)", 'reified_arg_check'); +$plain_arg_ns = bench("takesPlain(\$f)", 'plain_arg_check'); +compare("delta: reified arg check vs plain typed arg", $reified_arg_ns, "reif", $plain_arg_ns, "plain"); + +echo "\nNote: opcache is disabled (php -n). Same build, same interpreter\n"; +echo " path across all scenarios — deltas between rows are the signal.\n"; From 93b104a502529846c97f1aa5aaa2a377e594f547 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 20 May 2026 16:49:19 +0200 Subject: [PATCH 07/30] finish instanceof/catch impl Signed-off-by: Robert Landers --- .../namespace_visibility_parser_test.phpt | 38 +++++ .../reification/catch_with_t_ref.phpt | 26 +++ .../reification/instanceof_nested_t_ref.phpt | 26 +++ .../instanceof_with_t_ref_class_level.phpt | 38 +++++ .../instanceof_with_t_ref_function_level.phpt | 20 +++ Zend/zend_compile.c | 45 ++++-- Zend/zend_compile.h | 7 + Zend/zend_execute_API.c | 149 +++++++++++++++++- Zend/zend_opcode.c | 12 +- Zend/zend_vm_def.h | 10 +- Zend/zend_vm_execute.h | 40 ++--- 11 files changed, 369 insertions(+), 42 deletions(-) create mode 100644 Zend/tests/access_modifiers/namespace_visibility_parser_test.phpt create mode 100644 Zend/tests/generics/reification/catch_with_t_ref.phpt create mode 100644 Zend/tests/generics/reification/instanceof_nested_t_ref.phpt create mode 100644 Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt create mode 100644 Zend/tests/generics/reification/instanceof_with_t_ref_function_level.phpt diff --git a/Zend/tests/access_modifiers/namespace_visibility_parser_test.phpt b/Zend/tests/access_modifiers/namespace_visibility_parser_test.phpt new file mode 100644 index 000000000000..9838a951db49 --- /dev/null +++ b/Zend/tests/access_modifiers/namespace_visibility_parser_test.phpt @@ -0,0 +1,38 @@ +--TEST-- +Parser can distinguish namespace declaration from namespace visibility modifier +--FILE-- +helper(); +$session->count++; +echo "Count: {$session->count}\n"; + +// Test 3: Different namespace +namespace App\Other; + +$session2 = new \App\Auth\SessionManager(); +try { + $session2->helper(); + echo "ERROR: Should have thrown\n"; +} catch (\Error $e) { + echo "Correctly blocked: namespace visibility works\n"; +} + +?> +--EXPECT-- +Helper called +Count: 1 +Correctly blocked: namespace visibility works diff --git a/Zend/tests/generics/reification/catch_with_t_ref.phpt b/Zend/tests/generics/reification/catch_with_t_ref.phpt new file mode 100644 index 000000000000..e72cf3af9438 --- /dev/null +++ b/Zend/tests/generics/reification/catch_with_t_ref.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: catch (Box $e) matches only when the thrown exception's monomorph args agree with T +--FILE-- + extends Exception {} + +function tryCatch(Exception $e): string { + try { + throw $e; + } catch (BoxedError $caught) { + return "matched BoxedError"; + } catch (Exception $other) { + return "fell through"; + } +} + +echo tryCatch::(new BoxedError::('payload')), "\n"; // matched +echo tryCatch::(new BoxedError::('payload')), "\n"; // fell through: different mono +echo tryCatch::(new Exception('plain')), "\n"; // fell through: not a Box* +echo tryCatch::(new BoxedError::('s')), "\n"; // matched +?> +--EXPECT-- +matched BoxedError +fell through +fell through +matched BoxedError diff --git a/Zend/tests/generics/reification/instanceof_nested_t_ref.phpt b/Zend/tests/generics/reification/instanceof_nested_t_ref.phpt new file mode 100644 index 000000000000..eff113d3e711 --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_nested_t_ref.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: nested T-refs (instanceof Outer>) substitute through and resolve the outermost monomorph +--FILE-- + {} +class Outer {} + +class Holder { + public function isOuterOfBox(mixed $x): bool { + return $x instanceof Outer>; + } +} + +$intHolder = new Holder::(); +$strHolder = new Holder::(); + +var_dump($intHolder->isOuterOfBox(new Outer::>)); // true +var_dump($intHolder->isOuterOfBox(new Outer::>)); // false +var_dump($strHolder->isOuterOfBox(new Outer::>)); // true +var_dump($strHolder->isOuterOfBox(new Outer::)); // false: outer mono name differs +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) diff --git a/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt b/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt new file mode 100644 index 000000000000..6f7d1b17462c --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt @@ -0,0 +1,38 @@ + +--TEST-- +Reification: instanceof Box with class-level T resolves against the called scope's monomorph args +--FILE-- + {} + +class Outer { + public function isBox(mixed $x): bool { + return $x instanceof Box; + } +} + +$animalOuter = new Outer::(); +$dogOuter = new Outer::(); +$intOuter = new Outer::(); + +class Animal {} +class Dog extends Animal {} + +var_dump($animalOuter->isBox(new Box::)); // true +var_dump($animalOuter->isBox(new Box::)); // false: nominal monos +var_dump($dogOuter->isBox(new Box::)); // true +var_dump($dogOuter->isBox(new Box::)); // false +var_dump($intOuter->isBox(new Box::)); // true +var_dump($intOuter->isBox(new Box::)); // false +var_dump($animalOuter->isBox(42)); // false: not an object +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(false) diff --git a/Zend/tests/generics/reification/instanceof_with_t_ref_function_level.phpt b/Zend/tests/generics/reification/instanceof_with_t_ref_function_level.phpt new file mode 100644 index 000000000000..8e9707cf117e --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_with_t_ref_function_level.phpt @@ -0,0 +1,20 @@ +--TEST-- +Reification: instanceof Box with function-level T resolves against the current frame's turbofish bindings +--FILE-- + {} + +function isBoxOf(mixed $x): bool { + return $x instanceof Box; +} + +var_dump(isBoxOf::(new Box::)); // true +var_dump(isBoxOf::(new Box::)); // false +var_dump(isBoxOf::(new Box::));// true +var_dump(isBoxOf::(42)); // false: not an object +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 79c079b65acb..94cbe4ca33c9 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3981,8 +3981,10 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f /* Generic named type Box: when the args are concrete (no T-refs) * we resolve to the monomorph's canonical name "Box" so callers * like `instanceof Box` and `catch (Box $e)` distinguish - * monos. With T-refs we still need a runtime path (not yet implemented), - * so fall through to the bare-name behavior. */ + * monos. When the args contain T-refs (e.g. `instanceof Box`) we stash + * the pre-erasure type in the op_array's side table and emit a deferred + * fetch; the runtime substitutes T against the current frame's bindings + * and looks up (or synthesises) the resulting monomorph. */ if (name_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { zend_type ty = zend_compile_pre_erasure_typename(name_ast); if (!zend_type_contains_type_parameter(ty)) { @@ -3991,8 +3993,14 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f zend_type_release(ty, /* persistent */ false); return; } - zend_type_release(ty, /* persistent */ false); - name_ast = name_ast->child[0]; + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + uint32_t args_id = (table->turbofish_args + ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, ty); + result->op_type = IS_UNUSED; + result->u.op.num = zend_pack_generic_deferred_fetch(args_id, fetch_flags); + return; } if (name_ast->kind != ZEND_AST_ZVAL) { @@ -8374,20 +8382,35 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ if (!emitted_type_param) { zend_string *resolved_name = NULL; /* catch (Box): canonical monomorph name when args are - * concrete, matching the semantics of `instanceof Box`. */ + * concrete, matching the semantics of `instanceof Box`. + * catch (Box $e) with T-refs: stash the pre-erasure type and + * emit a deferred fetch; the runtime substitutes T against the + * current frame's bindings and resolves the monomorph. */ if (class_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { zend_type ty = zend_compile_pre_erasure_typename(class_ast); if (!zend_type_contains_type_parameter(ty)) { resolved_name = zend_type_to_canonical_string(ty); + zend_type_release(ty, /* persistent */ false); + } else { + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + uint32_t args_id = (table->turbofish_args + ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, ty); + opline->op1_type = IS_UNUSED; + opline->op1.num = zend_pack_generic_deferred_fetch(args_id, 0); + opline->extended_value = 0; + emitted_type_param = true; } - zend_type_release(ty, /* persistent */ false); } - if (!resolved_name) { - resolved_name = zend_resolve_class_name_ast(class_ast); + if (!emitted_type_param) { + if (!resolved_name) { + resolved_name = zend_resolve_class_name_ast(class_ast); + } + opline->op1_type = IS_CONST; + opline->op1.constant = zend_add_class_name_literal(resolved_name); + opline->extended_value = zend_alloc_cache_slot(); } - opline->op1_type = IS_CONST; - opline->op1.constant = zend_add_class_name_literal(resolved_name); - opline->extended_value = zend_alloc_cache_slot(); } if (var_name && zend_string_equals(var_name, ZSTR_KNOWN(ZEND_STR_THIS))) { diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 4f03f548a438..8697c5c520ef 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -215,6 +215,7 @@ ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type); +ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, uint32_t fetch_type); ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box); typedef union _zend_parser_stack_elem { @@ -1166,6 +1167,7 @@ ZEND_API bool zend_type_contains_type_parameter(zend_type type); #define ZEND_FETCH_CLASS_TRAIT 6 #define ZEND_FETCH_CLASS_TYPE_PARAM 7 /* function/method-level T */ #define ZEND_FETCH_CLASS_TYPE_PARAM_CLASS 8 /* class-level T (read from called scope's mono args) */ +#define ZEND_FETCH_CLASS_GENERIC_DEFERRED 9 /* generic application with T-refs (e.g. Box); resolves to a monomorph at runtime */ #define ZEND_FETCH_CLASS_MASK 0x0f #define ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT 16 @@ -1180,6 +1182,11 @@ static zend_always_inline uint32_t zend_pack_type_param_fetch(uint32_t param_ind return code | (param_index << ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT) | flags; } +static zend_always_inline uint32_t zend_pack_generic_deferred_fetch(uint32_t args_id, uint32_t flags) { + ZEND_ASSERT(args_id < (1u << (32 - ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT))); + return ZEND_FETCH_CLASS_GENERIC_DEFERRED | (args_id << ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT) | flags; +} + static zend_always_inline uint32_t zend_unpack_type_param_index(uint32_t fetch_type) { return fetch_type >> ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT; } diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index ad6f32b9e6ac..c281f82fe29a 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -38,6 +38,7 @@ #include "zend_observer.h" #include "zend_call_stack.h" #include "zend_frameless_function.h" +#include "zend_smart_str.h" #ifdef HAVE_SYS_TIME_H #include #endif @@ -1764,7 +1765,21 @@ ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, } if (table && param_index < table->count && table->names[param_index]) { - return zend_fetch_class_by_name(table->names[param_index], NULL, fetch_type); + /* Try the bound binding as a class name. If it isn't a class (e.g. + * scalar T-arg like `int`) the lookup returns NULL silently and we + * fall through to the parameter's declared bound — which is the only + * remaining thing a bare T-ref can resolve to. */ + uint32_t silent_fetch = (fetch_type & ~ZEND_FETCH_CLASS_EXCEPTION) + | ZEND_FETCH_CLASS_SILENT | ZEND_FETCH_CLASS_NO_AUTOLOAD; + zend_class_entry *ce = zend_fetch_class_by_name(table->names[param_index], NULL, silent_fetch); + if (ce) return ce; + /* Not in the class table yet — retry with autoload before giving up, + * but only if the original fetch allowed it. */ + if (!(fetch_type & ZEND_FETCH_CLASS_NO_AUTOLOAD)) { + ce = zend_fetch_class_by_name(table->names[param_index], NULL, + (fetch_type & ~ZEND_FETCH_CLASS_EXCEPTION) | ZEND_FETCH_CLASS_SILENT); + if (ce) return ce; + } } /* Resolution failed. Suppress the diagnostic when SILENT and no thrown @@ -1795,6 +1810,135 @@ ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, return NULL; } +/* Look up the function-level T-ref binding (current frame's T-table); returns + * NULL when the table is missing the slot, in which case the caller should + * fall back to the parameter's declared bound. */ +static zend_string *zend_resolve_fn_type_param_name(uint32_t index) +{ + zend_execute_data *ex = EG(current_execute_data); + if (!ex || !ZEND_USER_CODE(ex->func->type)) return NULL; + zend_type_arg_table *table = ex->type_args; + if (table && index < table->count && table->names[index]) { + return table->names[index]; + } + return NULL; +} + +/* Look up the class-level T-ref binding. The binding lives on the monomorph + * of the lexical scope — see the matching walk in zend_resolve_generic_type_param. */ +static zend_string *zend_resolve_class_type_param_name(uint32_t index) +{ + zend_execute_data *ex = EG(current_execute_data); + zend_class_entry *lexical = ex ? ex->func->common.scope : NULL; + zend_class_entry *cur = ex ? zend_get_called_scope(ex) : NULL; + while (cur && cur->parent != lexical) { + cur = cur->parent; + } + if (cur && cur->generic_type_args && index < cur->generic_type_args->count) { + return cur->generic_type_args->names[index]; + } + return NULL; +} + +/* Resolve a function/method-level generic type parameter's name to its bound's + * class name; for class-level params the bound is consulted on the lexical + * scope's parameter list. Used by deferred-generic substitution to fall back + * when no concrete binding was supplied at the call site. */ +static zend_string *zend_resolve_type_param_bound_name(const zend_type_parameter_ref *ref) +{ + zend_generic_parameter_list *params = NULL; + zend_execute_data *ex = EG(current_execute_data); + if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + zend_class_entry *lexical = ex ? ex->func->common.scope : NULL; + if (lexical) params = lexical->generic_parameters; + } else if (ex && ZEND_USER_CODE(ex->func->type)) { + params = ex->func->op_array.generic_parameters; + } + if (!params || ref->index >= params->count) return NULL; + zend_type bound = params->parameters[ref->index].bound; + if (ZEND_TYPE_HAS_NAME(bound)) return ZEND_TYPE_NAME(bound); + return NULL; +} + +/* Recursively append the canonical text for a substituted type fragment. The + * walk mirrors zend_generic_canonical_class_name, but substitutes T-refs + * against the current frame's bindings as it goes. Sets *failed when a + * binding is missing and no class bound is available. */ +static void zend_append_substituted_canonical(smart_str *buf, const zend_type *t, bool *failed) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*t)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*t); + zend_string *name = (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) + ? zend_resolve_class_type_param_name(ref->index) + : zend_resolve_fn_type_param_name(ref->index); + if (!name) name = zend_resolve_type_param_bound_name(ref); + if (!name) { + *failed = true; + return; + } + smart_str_append(buf, name); + return; + } + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*t)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*t); + if (nwa->name) smart_str_append(buf, nwa->name); + smart_str_appendc(buf, '<'); + for (uint32_t i = 0; i < nwa->count; i++) { + if (i > 0) smart_str_appendc(buf, ','); + zend_append_substituted_canonical(buf, &nwa->args[i], failed); + if (*failed) return; + } + smart_str_appendc(buf, '>'); + return; + } + /* Plain leaf (class name or pure scalar): defer to the canonical formatter. */ + zend_string *piece = zend_type_to_canonical_string(*t); + smart_str_append(buf, piece); + zend_string_release(piece); +} + +/* Resolve `instanceof Box` / `catch (Box $e)` and friends at runtime. + * Reads the compile-time pre-erasure type from the executing op_array's + * side table, substitutes T-refs against the current frame's bindings, and + * looks up the resulting monomorph (synthesizing it if needed via the + * standard class-lookup path). */ +ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, uint32_t fetch_type) +{ + zend_execute_data *ex = EG(current_execute_data); + bool suppress = (fetch_type & ZEND_FETCH_CLASS_SILENT) + && !(fetch_type & ZEND_FETCH_CLASS_EXCEPTION); + if (!ex || !ZEND_USER_CODE(ex->func->type)) { + if (suppress) return NULL; + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve deferred generic type at runtime: not in a user function"); + return NULL; + } + + const zend_type *boxed = zend_generic_get_turbofish_args(&ex->func->op_array, args_id); + if (!boxed || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + if (suppress) return NULL; + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve deferred generic type at runtime: missing side-table entry"); + return NULL; + } + + smart_str buf = {0}; + bool failed = false; + zend_append_substituted_canonical(&buf, boxed, &failed); + if (failed) { + smart_str_free(&buf); + if (suppress) return NULL; + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve deferred generic type at runtime: no binding supplied for one of its type parameters and the parameter has no class bound"); + return NULL; + } + smart_str_0(&buf); + zend_string *canonical = buf.s; + zend_class_entry *ce = zend_fetch_class_by_name(canonical, NULL, fetch_type); + zend_string_release(canonical); + return ce; +} + zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type) /* {{{ */ { zend_class_entry *ce, *scope; @@ -1806,6 +1950,9 @@ zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type) case ZEND_FETCH_CLASS_TYPE_PARAM_CLASS: return zend_resolve_generic_type_param( zend_unpack_type_param_index(fetch_type), fetch_type); + case ZEND_FETCH_CLASS_GENERIC_DEFERRED: + return zend_resolve_deferred_generic_class( + zend_unpack_type_param_index(fetch_type), fetch_type); case ZEND_FETCH_CLASS_SELF: scope = zend_get_executed_scope(); if (UNEXPECTED(!scope)) { diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 4a8c92888d07..9d79e0159663 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -312,13 +312,15 @@ ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table) { efree(table); } -/* Returns NULL when the type is not a concrete class reference — the resolver - * reads NULL as "fall back to the parameter's bound". */ +/* Always returns the canonical name of the bound type — class names, monomorph + * applications (`Box`), scalars (`int`, `string`), and unions/intersections. + * Callers that only care about class-name dispatch should themselves check + * whether the lookup succeeded. Returns NULL only for unset/empty types. */ ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type) { - if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type) || ZEND_TYPE_HAS_NAME(type)) { - return zend_type_to_canonical_string(type); + if (!ZEND_TYPE_IS_SET(type)) { + return NULL; } - return NULL; + return zend_type_to_canonical_string(type); } ZEND_API void zend_free_internal_arg_info(zend_internal_function *function, diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 58c4b0e2e297..1af0cb8fbcf3 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4860,11 +4860,11 @@ ZEND_VM_HANDLER(107, ZEND_CATCH, CONST|UNUSED, JMP_ADDR, LAST_CATCH|CACHE_SLOT) ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (OP1_TYPE == IS_UNUSED) { - /* catch (T $e): T resolved per execution against the runtime T-table - * — not cached, since the same opline runs in monos with different - * bindings. */ - catch_ce = zend_resolve_generic_type_param( - zend_unpack_type_param_index(opline->op1.num), + /* catch (T $e) / catch (Box $e): the class resolves per execution + * against the runtime T-table — not cached, since the same opline + * runs in monos with different bindings. zend_fetch_class dispatches + * to the right resolver based on the packed sub-type. */ + catch_ce = zend_fetch_class(NULL, opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index aad1aed1e214..1bef2068eebb 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5191,11 +5191,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_CO ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (IS_CONST == IS_UNUSED) { - /* catch (T $e): T resolved per execution against the runtime T-table - * — not cached, since the same opline runs in monos with different - * bindings. */ - catch_ce = zend_resolve_generic_type_param( - zend_unpack_type_param_index(opline->op1.num), + /* catch (T $e) / catch (Box $e): the class resolves per execution + * against the runtime T-table — not cached, since the same opline + * runs in monos with different bindings. zend_fetch_class dispatches + * to the right resolver based on the packed sub-type. */ + catch_ce = zend_fetch_class(NULL, opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); @@ -32953,11 +32953,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_UN ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (IS_UNUSED == IS_UNUSED) { - /* catch (T $e): T resolved per execution against the runtime T-table - * — not cached, since the same opline runs in monos with different - * bindings. */ - catch_ce = zend_resolve_generic_type_param( - zend_unpack_type_param_index(opline->op1.num), + /* catch (T $e) / catch (Box $e): the class resolves per execution + * against the runtime T-table — not cached, since the same opline + * runs in monos with different bindings. zend_fetch_class dispatches + * to the right resolver based on the packed sub-type. */ + catch_ce = zend_fetch_class(NULL, opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); @@ -58247,11 +58247,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_CONST_T ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (IS_CONST == IS_UNUSED) { - /* catch (T $e): T resolved per execution against the runtime T-table - * — not cached, since the same opline runs in monos with different - * bindings. */ - catch_ce = zend_resolve_generic_type_param( - zend_unpack_type_param_index(opline->op1.num), + /* catch (T $e) / catch (Box $e): the class resolves per execution + * against the runtime T-table — not cached, since the same opline + * runs in monos with different bindings. zend_fetch_class dispatches + * to the right resolver based on the packed sub-type. */ + catch_ce = zend_fetch_class(NULL, opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); @@ -85807,11 +85807,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_UNUSED_ ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (IS_UNUSED == IS_UNUSED) { - /* catch (T $e): T resolved per execution against the runtime T-table - * — not cached, since the same opline runs in monos with different - * bindings. */ - catch_ce = zend_resolve_generic_type_param( - zend_unpack_type_param_index(opline->op1.num), + /* catch (T $e) / catch (Box $e): the class resolves per execution + * against the runtime T-table — not cached, since the same opline + * runs in monos with different bindings. zend_fetch_class dispatches + * to the right resolver based on the packed sub-type. */ + catch_ce = zend_fetch_class(NULL, opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); From fe13b673f7d488fb96071dfe5be23c14325af770 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 20 May 2026 17:12:16 +0200 Subject: [PATCH 08/30] documentation Signed-off-by: Robert Landers --- UPGRADING | 40 +++++-- UPGRADING.INTERNALS | 103 ++++++++++++++-- docs/source/core/generics.rst | 220 ++++++++++++++++++++++++++-------- 3 files changed, 292 insertions(+), 71 deletions(-) diff --git a/UPGRADING b/UPGRADING index b0b7f1cca960..77ff2d445804 100644 --- a/UPGRADING +++ b/UPGRADING @@ -195,13 +195,27 @@ PHP 8.6 UPGRADE NOTES `static`, `parent`). Recursive bounds (`T : Comparable`) are supported. Anonymous classes cannot declare type parameters. - At runtime each type parameter is replaced by its declared bound - (or `mixed` when unbounded, or when the bound is invalid in the - target position, e.g. `callable` on a property), and type - arguments are discarded. Pre-erasure metadata is preserved on - functions, methods, and class entries and is exposed through - Reflection so that PHP-based static-analysis tools can consume - generics without re-parsing source. + Generics are reified along two axes. Class-like generics are + monomorphised: each distinct argument tuple (`Box`, + `Box`) synthesises a separate class entry whose canonical + name is spelled out (`"Box"`), registered in the class table, + and visible through `get_class()`, `var_dump`, and + `instanceof Box`. Function- and method-level generics are + reified per call frame: each call carries its own type-argument + table, with no persistent `id` function entry (avoiding + unbounded growth). Closures and generators preserve the table so + it survives suspension. + + Ordinary parameter, return, and property type-checks run against + each parameter's declared *bound* (or `mixed` when unbounded, or + when the bound is invalid in the target position - e.g. `callable` + on a property). The bound is the answer to "what can a caller + actually pass at this slot." Reified type arguments are kept on a + side table and consulted at well-defined runtime points: monomorph + synthesis, turbofish argument validation, deferred lookups for + `instanceof Box` / `catch (Box $e)`, bare `T`-ref resolution + (`instanceof T`, `catch (T $e)`, `new T()`, `T::method()`), + inheritance linking, and Reflection. . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. . It is now possible to define the `__debugInfo()` magic method on enums. @@ -355,8 +369,12 @@ PHP 8.6 UPGRADE NOTES / getTraits() APIs). . Added ReflectionNamedType::hasGenericArguments() and ReflectionNamedType::getGenericArguments(). The arguments are returned - as ReflectionType instances in source order (pre-erasure form); - ReflectionNamedType::getName() continues to return the erased name. + as ReflectionType instances in source order (reified form); + ReflectionNamedType::getName() returns the bound's name - i.e. the + type a caller can actually pass at the slot - so it stays useful for + runtime checks. Use getGenericArguments() to recover the reified + shape, and look for ReflectionTypeParameterReference inside the + returned list when a slot is itself a type-parameter reference. - Intl: . `grapheme_strrev()` returns strrev for grapheme cluster unit. @@ -393,8 +411,8 @@ PHP 8.6 UPGRADE NOTES ReflectionClass::getGenericParameters() and ReflectionFunctionAbstract::getGenericParameters()). . ReflectionTypeParameterReference (extends ReflectionType, appears only - inside pre-erasure type expressions: bounds, defaults, and the elements - of ReflectionNamedType::getGenericArguments()). + inside reified type expressions: bounds, defaults, and the elements of + ReflectionNamedType::getGenericArguments()). . enum ReflectionGenericVariance { Invariant; Covariant; Contravariant }. - Standard: diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index d06308b18bf0..3fcbd2cdbc3f 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -117,8 +117,8 @@ PHP 8.6 INTERNALS UPGRADE NOTES . Added ZEND_CONTAINER_OF(). . The OPENBASEDIR_CHECKPATH() compatibility macro has been removed, instead use php_check_open_basedir() directly. - . Added support for runtime-bound-checked generic type parameters. The - main additions: + . Added support for reified generic type parameters. The main + additions: . New types in zend_compile.h: zend_generic_parameter, zend_generic_parameter_list, zend_generic_type_table, and zend_generic_scope_entry. Allocate / destroy via @@ -128,11 +128,14 @@ PHP 8.6 INTERNALS UPGRADE NOTES zend_generic_type_table_destroy(). . zend_op_array and zend_class_entry both gained an optional `generic_parameters` (declared parameter list) and an optional - `generic_types` side table holding the pre-erasure forms of - return types, parameter types, property types, class-constant - types, the extends type, implements list, and trait-use list. - The runtime arg_info / property / class-constant slots continue - to hold only the erased form. + `generic_types` side table holding the reified forms of return + types, parameter types, property types, class-constant types, + the extends type, implements list, and trait-use list. The + runtime arg_info / property / class-constant slots continue to + hold the bound view (the parameter's declared bound, with T-refs + resolved); the reified side table is consulted at monomorph + synthesis, ZEND_VERIFY_GENERIC_ARGUMENTS, the new deferred + class-fetch path, inheritance linking, and Reflection. . New AST kinds: ZEND_AST_GENERIC_TYPE_PARAMETER_LIST, ZEND_AST_GENERIC_TYPE_PARAMETER, ZEND_AST_GENERIC_NAMED_TYPE, ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST, ZEND_AST_TURBOFISH. @@ -150,11 +153,91 @@ PHP 8.6 INTERNALS UPGRADE NOTES zend_type_parameter_ref { zend_string *name; uint32_t index; uint8_t origin; } and zend_type_named_with_args { zend_string *name; uint32_t name_attr; uint32_t count; zend_type args[]; }. - These bits only ever appear in pre-erasure forms held by the - side table; runtime arg_info / property / class-constant types - never carry them. Helpers: ZEND_TYPE_HAS_TYPE_PARAMETER(), + These bits only ever appear in reified forms held by the side + table; runtime arg_info / property / class-constant types never + carry them. Helpers: ZEND_TYPE_HAS_TYPE_PARAMETER(), ZEND_TYPE_TYPE_PARAMETER(), ZEND_TYPE_HAS_NAMED_WITH_ARGS(), ZEND_TYPE_NAMED_WITH_ARGS(). + . Class-level reification (monomorphs). Each distinct argument + tuple at a generic class, interface, or trait synthesises a + separate zend_class_entry registered in EG(class_table) under a + canonical name ("Box", "Map>"). The + canonical encoding uses `<...>` in the class name, which is + otherwise invalid in user-declared classes; the bit is detected + with zend_class_name_is_monomorph() / zend_class_is_monomorph(). + New API: + - zend_synthesize_monomorph(base, args, arity): synthesise (or + return the cached) class entry; idempotent on canonical + tuples. + - zend_get_defaults_monomorph(base): the monomorph built from + the parameters' declared defaults; NULL when any parameter + has no default. Used by ZEND_NEW for `new static()` and + dynamic `new $name()`. + - zend_try_synthesize_monomorph_by_name(name, flags): parses a + canonical-shaped name and synthesises on demand. Hooked into + zend_lookup_class_ex so unserialize, dynamic new, and + class_exists() all materialise monomorphs transparently. + - zend_type_to_canonical_string(type), + zend_generic_canonical_class_name(base_name, args, arity), + zend_type_contains_type_parameter(type) — the canonical-name + builder is permutation-stable on union/intersection order so + equivalent type-arg lists hash to the same entry. + - zend_generic_get_or_create_class_table(ce) — exposed so + extensions can populate the side table during synthesis. + - ZEND_ACC_GENERIC_ALL_DEFAULTS (1u << 31) on ce_flags — hot + path bit for ZEND_NEW to skip the per-parameter default + scan. + . Function- and method-level reification (per call frame). Each + generic call installs a fresh table mapping parameter index to + bound class name: + typedef struct _zend_type_arg_table { + uint32_t count; + zend_string *names[1]; + } zend_type_arg_table; + Allocate / destroy via zend_type_arg_table_alloc(count) and + zend_type_arg_table_destroy(t). zend_execute_data gained a + `type_args` field initialised to NULL by + zend_vm_init_call_frame() and released by + zend_vm_stack_free_call_frame_ex(). The table survives closure + and generator suspension. There is no persistent function-level + monomorph — the binding lives on the frame. + Population API: + - zend_type_arg_canonical_name(type): canonical-name for a + single type-arg slot, including scalars (`"int"`, + `"?string"`). Returns NULL only for unset types. + - zend_build_generic_call_type_args(call, args_box): builds + the table from a turbofish NAMED_WITH_ARGS carrier (or from + declared defaults for unsupplied slots). + - zend_verify_generic_arg_types(call, args_box): validates + each arg against the corresponding parameter's bound. + - zend_resolve_generic_type_param(index, fetch_type): runtime + T-ref resolver. Walks EX(type_args) for function-level T + and the called scope -> lexical scope chain for class-level + T (the binding lives on the monomorph that is the direct + child of the lexical scope on cur->generic_type_args). + zend_generic_parameter_list gained an `inferable_mask` (bit i + set when some value-parameter's reified type is exactly param i + at a top-level position) so inference can short-circuit. + . Three new class-fetch sub-types on the class-fetch enum, + packed into op.num via zend_pack_*_fetch helpers: + - ZEND_FETCH_CLASS_TYPE_PARAM (7) — function/method- + level bare T-ref (`instanceof T`, `catch (T $e)`, + `new T()`, `T::method()`). + - ZEND_FETCH_CLASS_TYPE_PARAM_CLASS (8) — class-level bare + T-ref; resolves via the called-scope walk. + - ZEND_FETCH_CLASS_GENERIC_DEFERRED (9) — generic named type + whose args contain T-refs (`instanceof Box`, + `catch (Box $e)`). The args_id sits in the high bits of + op.num and points into the op_array's + generic_types->turbofish_args side table. + All three are detected with zend_fetch_is_type_param() (the + first two) or by the sub-type directly; packed with + zend_pack_type_param_fetch(idx, flags, class_level) and + zend_pack_generic_deferred_fetch(args_id, flags); unpacked with + zend_unpack_type_param_index(fetch_type). Routed through + zend_fetch_class, which dispatches to + zend_resolve_generic_type_param() or + zend_resolve_deferred_generic_class(). . New compiler-globals fields: CG(type_arg_depth) (right-angle split state used by the zendlex wrapper), CG(token_residual) (single-token pushback slot), and CG(generic_scope) (linked diff --git a/docs/source/core/generics.rst b/docs/source/core/generics.rst index fdc0cf97210e..569759694313 100644 --- a/docs/source/core/generics.rst +++ b/docs/source/core/generics.rst @@ -3,18 +3,49 @@ ############### PHP supports generic type parameters on classes, interfaces, traits, functions, methods, closures, -and arrow functions. They are *bound-erased*: at runtime, each type parameter is replaced by its -declared bound (or ``mixed`` when unbounded), so the engine sees ordinary PHP types. The -pre-erasure form is kept in a per-entity side table and consulted by the linker, the runtime -turbofish check, and Reflection. Existing handlers - parameter checks, return-type verification, -property writes - operate on the erased type and never see a ``T``-ref. - -*************************** - Pre-erasure type carriers -*************************** - -``zend_type`` carries the pre-erasure shapes generics need. Two bits in ``type_mask`` distinguish -them: +and arrow functions. They are *reified*, but reified along two different axes depending on the +declaring entity: + +- **Class-like generics are monomorphised.** Each distinct argument tuple at a generic class, + interface, or trait synthesises a separate ``zend_class_entry`` — ``Box`` and + ``Box`` are distinct classes whose canonical names are spelled out + (``"Box"``, ``"Box"``) and registered in ``EG(class_table)``. The monomorph extends + (or implements, or uses) the base class with the substitution baked in. ``get_class()``, + ``var_dump``, and ``$x instanceof Box`` all see the canonical mono. + +- **Function- and method-level generics are reified per call frame.** Each call gets a fresh + ``zend_type_arg_table`` on ``EX(type_args)`` mapping parameter index to bound class name. There + is no persistent ``id`` function entry — function-level monomorphs would grow without + bound and require a lot of bookkeeping, so the binding lives on the frame and is consulted on + demand. Closures and generators preserve the table so it survives suspension. + +A parameter's **bound** is the type-check the engine uses on ordinary parameter / return / +property slots: ``arg_info::type``, ``zend_property_info::type``, and ``zend_class_constant::type`` +carry the bound, and the existing runtime handlers (parameter coercion, return verification, +property writes) walk them as if no generics were involved. The bound is also the answer to "what +can a caller actually pass at this slot", which is why Reflection's ``ReflectionType::getName()`` +and ``__toString()`` continue to return the bound's text. Reflection exposes the reified shape +separately through ``ReflectionNamedType::getGenericArguments()`` and +``ReflectionTypeParameterReference``. + +The reified shape is held on a side table keyed by entity slot. The runtime reads it at a small +number of well-defined points: + +- monomorph synthesis (``zend_synthesize_monomorph``), +- turbofish argument validation (``ZEND_VERIFY_GENERIC_ARGUMENTS``), +- deferred class lookups for ``instanceof Box`` / ``catch (Box $e)`` (the new + ``ZEND_FETCH_CLASS_GENERIC_DEFERRED`` sub-type), +- bare ``T``-ref resolution for ``instanceof T`` / ``catch (T $e)`` / ``new T()`` / + ``T::method()``, +- inheritance linking, where a parent's reified type is substituted with the child's bindings, +- Reflection. + +*********************** + Reified type carriers +*********************** + +``zend_type`` carries the reified shapes generics need on side-table slots. Two bits in +``type_mask`` distinguish them: .. code:: c @@ -41,7 +72,7 @@ when ``_ZEND_TYPE_NAMED_WITH_ARGS_BIT`` is set, ``t.ptr`` is a generic applicati } zend_type_named_with_args; The ``origin`` field on a ``T``-ref tells you which scope to resolve ``index`` in. NAMED_WITH_ARGS -values with ``name == NULL`` are synthetic carriers that hold the args at a turbofish site - +values with ``name == NULL`` are synthetic carriers that hold the args at a turbofish site — they're not type expressions, just a payload. Both payloads only appear in side-table slots, are heap-allocated, and are released through ``zend_type_release``. @@ -55,23 +86,27 @@ both ``NULL`` on entities that don't use generics: .. code:: c zend_generic_parameter_list *generic_parameters; /* the entity's own declaration */ - zend_generic_type_table *generic_types; /* pre-erasure metadata for everything else */ + zend_generic_type_table *generic_types; /* reified shapes for every other slot */ ``generic_parameters`` is the entity's own declaration list (a flexible-array struct allocated -through ``zend_generic_parameter_list_alloc``). Each parameter: +through ``zend_generic_parameter_list_alloc``). Each parameter carries two views of its bound and +default: the ``bound`` / ``default_type`` slots hold the form the ordinary type-check machinery +sees (a class name or scalar mask, with ``T``-refs resolved to their bounds), and the +``..._pre_erasure`` slots (the C field names are kept from earlier scaffolding) hold the reified +shape when it differs from the bound view: .. code:: c typedef struct _zend_generic_parameter { zend_string *name; zend_generic_variance variance; /* INVARIANT, COVARIANT, CONTRAVARIANT */ - zend_type bound; /* erased view; NONE if unbounded */ + zend_type bound; /* type-check view; NONE if unbounded */ zend_type bound_pre_erasure; /* NONE if same as bound */ zend_type default_type; zend_type default_pre_erasure; } zend_generic_parameter; -``generic_types`` collects every other place a pre-erasure type might need to be looked up: +``generic_types`` collects every other slot where a reified type might need to be looked up: .. code:: c @@ -83,18 +118,19 @@ through ``zend_generic_parameter_list_alloc``). Each parameter: HashTable *class_constants; HashTable *implements; /* implements index -> zend_type * */ HashTable *trait_uses; /* trait-use index -> zend_type * */ - HashTable *turbofish_args; /* opline->extended_value -> zend_type * */ + HashTable *turbofish_args; /* args_id -> zend_type * */ } zend_generic_type_table; Every slot is independently NULLable. The table is allocated lazily by ``zend_generic_get_or_create_class_table`` / ``..._op_array_table`` on first use, and individual -slots stay ``NULL`` whenever the pre-erasure form would be byte-equal to the erased one. You -populate slots through the ``zend_generic_type_table_set_*`` family; each setter takes ownership -of the ``zend_type`` you hand it. +slots stay ``NULL`` whenever the reified form would be byte-equal to the bound view. You populate +slots through the ``zend_generic_type_table_set_*`` family; each setter takes ownership of the +``zend_type`` you hand it. -The ``turbofish_args`` table is keyed by ``opline->extended_value`` rather than the opline's -position in the bytecode array. Optimizer passes reorder, insert, and delete opcodes; the -``extended_value`` operand belongs to the opline itself, so it survives reordering. +The ``turbofish_args`` table is keyed by an ``args_id`` rather than the opline's position in the +bytecode array. Optimizer passes reorder, insert, and delete opcodes; the id is stamped into the +opline itself (either ``extended_value`` for ``ZEND_VERIFY_GENERIC_ARGUMENTS`` or the packed +``op.num`` for ``ZEND_FETCH_CLASS_GENERIC_DEFERRED``), so the id survives reordering. Attributes carry their turbofish on two fields on ``zend_attribute``: @@ -114,10 +150,10 @@ parameter whose bound or default is currently being compiled, which is what lets reject ``class A`` (top-level self-reference) while accepting ``class A>`` (the inner ``T`` is nested under another generic). -Every type expression carrying a ``T`` is compiled twice: once as the runtime-erased ``zend_type`` -on the entity's ordinary slot (``arg_info::type``, ``zend_property_info::type``), and once in -pre-erasure form on the matching ``generic_types`` slot. When the bound is a list type (union, -intersection, or DNF), the erased copy is built through ``zend_arena_deep_copy_type_list`` so +Every type expression carrying a ``T`` is compiled twice: once as the bound-view ``zend_type`` on +the entity's ordinary slot (``arg_info::type``, ``zend_property_info::type``), and once in +reified form on the matching ``generic_types`` slot. When the bound is a list type (union, +intersection, or DNF), the bound copy is built through ``zend_arena_deep_copy_type_list`` so nested type lists are not aliased between ``param->bound`` and the consuming ``arg_info``. For a turbofish at a call or ``new`` site, ``zend_emit_verify_generic_arguments`` compiles the @@ -126,6 +162,13 @@ under a fresh ``args_id``, and emits a ``ZEND_VERIFY_GENERIC_ARGUMENTS`` opcode ``op2.num = arity`` and ``extended_value = args_id``. The opcode sits between the call's ``INIT_*`` (or ``ZEND_NEW``) and its ``DO_*``, so the call frame is set up when the handler runs. +For a generic named type in expression position — ``instanceof Box``, ``catch (Box $e)``, +``new Box::()`` — ``zend_compile_class_ref`` does compile-time canonicalisation when the +args are fully concrete (no ``T``-refs): it produces the canonical name string and emits an +``IS_CONST`` operand, so the lookup is the same constant-cache hit any other class reference +gets. When the args contain ``T``-refs the resolution has to wait for the call frame's bindings; +see *Deferred generic class resolution* below. + ****************************** Variance and static context ****************************** @@ -145,27 +188,80 @@ invariant. Constructors are exempt. Violations produce ``Type parameter T declar The static-context check is structural: ``zend_check_class_origin_in_static_context`` rejects any class-origin ``T``-ref that resolves while ``CG(in_static_member_type)`` is set. The flag is set in ``zend_compile_func_decl`` around the signature of a static method and in -``zend_compile_prop_decl`` around the type of a static property - a static member has no instance, +``zend_compile_prop_decl`` around the type of a static property — a static member has no instance, so an instance-bound parameter has no binding to consult. -***************** - Runtime handler -***************** +****************** + Runtime handlers +****************** + +``ZEND_VERIFY_GENERIC_ARGUMENTS`` +--------------------------------- -The ``ZEND_VERIFY_GENERIC_ARGUMENTS`` handler lives in ``zend_vm_def.h``. It looks up the captured -args via ``zend_generic_get_turbofish_args`` and dispatches to ``zend_check_generic_call_arguments`` +The handler lives in ``zend_vm_def.h``. It looks up the captured args via +``zend_generic_get_turbofish_args`` and dispatches to ``zend_check_generic_call_arguments`` (call) or ``zend_check_generic_new_arguments`` (instantiation). Both validate arity against the callee's ``generic_parameters``, then walk the carrier and call ``zend_check_generic_arg_satisfies_bound`` for each ``(arg, parameter)`` pair. Mismatches throw ``ArgumentCountError`` or ``TypeError``; on exception, teardown releases the call frame, the trampoline name if any, and ``$this`` if held. +When validation passes, the handler installs the bindings: for calls, +``zend_build_generic_call_type_args`` builds the ``zend_type_arg_table`` and assigns it to +``call->type_args``; for instantiation, the canonical monomorph is synthesised (or fetched from +the class table) and ``object_init_ex`` runs against it. + +Bare ``T``-ref resolution +------------------------- + +``instanceof T`` / ``catch (T $e)`` / ``new T()`` / ``T::method()`` emit an ``IS_UNUSED`` operand +whose ``op.num`` is packed by ``zend_pack_type_param_fetch`` (sub-type +``ZEND_FETCH_CLASS_TYPE_PARAM`` or ``..._TYPE_PARAM_CLASS``, parameter index in the high bits). +``zend_resolve_generic_type_param`` does the lookup at runtime: + +- Function/method-level: ``EX(type_args)->names[index]`` is the canonical class name supplied + for that parameter at the call site. +- Class-level: walk from the called scope upward until you reach a direct child of the lexical + scope — that child *is* the monomorph that owns the binding, on ``ce->generic_type_args``. + +If the table doesn't have a binding for that slot, the resolver falls back to the parameter's +declared bound. ``zend_type_arg_canonical_name`` writes the full canonical name into the table +(including scalars), and the resolver retries via ``zend_fetch_class_by_name`` silently — a +scalar name like ``"int"`` simply fails to resolve as a class and triggers the bound fallback, +which is fine for the cases where the bound has a class name. + +Deferred generic class resolution +--------------------------------- + +``instanceof Box`` and ``catch (Box $e)`` — generic named types whose argument list still +contains ``T``-refs at compile time — can't be canonicalised statically because the binding for +``T`` only exists per call frame. The compiler: + +1. Compiles the reified ``zend_type`` (``Box`` as a NAMED_WITH_ARGS with a T-ref child). +2. Stashes it in the op_array's ``generic_types->turbofish_args`` under a fresh ``args_id``. +3. Emits an ``IS_UNUSED`` operand whose ``op.num`` packs ``ZEND_FETCH_CLASS_GENERIC_DEFERRED`` + and the ``args_id`` (via ``zend_pack_generic_deferred_fetch``). + +At runtime, ``zend_fetch_class`` dispatches on the sub-type to +``zend_resolve_deferred_generic_class``, which: + +1. Reads the boxed type from the executing op_array's ``turbofish_args`` table. +2. Recursively walks the NAMED_WITH_ARGS, substituting each ``T``-ref against the current + frame's bindings (function-level via ``EX(type_args)``, class-level via the same walk as + bare ``T``-ref resolution). +3. Builds the canonical name string (``"Box"``, ``"Outer>"``, etc.). +4. Hands it to ``zend_fetch_class_by_name``, which finds the existing monomorph or synthesises + one through ``zend_try_synthesize_monomorph_by_name``. + +``ZEND_INSTANCEOF`` and ``ZEND_CATCH`` already accept ``IS_UNUSED`` operands and route through +``zend_fetch_class``; the deferred dispatch is just a new sub-type case there. + ************************ Inheritance and linking ************************ When a child extends a generic ancestor or uses a generic trait, the linker substitutes the -inherited prototype's pre-erasure types with the child's bindings before the variance check runs. +inherited prototype's reified types with the child's bindings before the variance check runs. ``zend_get_inheritance_binding`` returns the *direct* binding (the args at ce's own ``extends``/``implements``/``use`` clause); for ``extends`` it matches by pointer when ``ZEND_ACC_RESOLVED_PARENT`` is set and by name otherwise, since the override check fires before @@ -181,7 +277,7 @@ it returns the unsubstituted fallback when ce isn't generic, when the binding lo when substitution would yield another ``T``-ref because the inheriting class forwards the parameter. ``zend_get_function_declaration`` takes an optional ``subst_ce``: when present, each printed type runs through ``zend_substitute_proto_type`` so the LSP error message shows the -substituted parent signature rather than the bound-erased form. +substituted parent signature rather than the bound view. *************************** Member substitution sites @@ -192,34 +288,57 @@ Substitution is applied at five sites. Each clones the parent's ``zend_property_ with the parent via refcount. Clones keep ``ce`` set to the parent's defining class so ``zend_opcode.c``'s teardown does not double-release shared fields. -- ``do_inherit_property`` - property type on a class extending a generic parent. -- Property hook signatures - the ``get`` return slot and the ``set`` value-parameter slot. -- ``zend_do_traits_property_binding`` - trait property type, with the using class's binding. -- ``zend_add_trait_method`` - trait method signature, via ``zend_substitute_trait_method_arg_info``. -- ``do_inherit_method`` - non-overridden inherited method signature, via +- ``do_inherit_property`` — property type on a class extending a generic parent. +- Property hook signatures — the ``get`` return slot and the ``set`` value-parameter slot. +- ``zend_do_traits_property_binding`` — trait property type, with the using class's binding. +- ``zend_add_trait_method`` — trait method signature, via ``zend_substitute_trait_method_arg_info``. +- ``do_inherit_method`` — non-overridden inherited method signature, via ``zend_maybe_substitute_inherited_method``. Body opcodes are not re-emitted: ``VERIFY_*`` opcodes were laid down at the parent's compile time against the unsubstituted view, so the child observes the substituted signature on the clone but the parent's original opcodes inside. -Diamond detection is in ``zend_validate_generic_diamond_bindings``. It runs *before* ``ce->parent`` is set and -``ZEND_ACC_RESOLVED_PARENT`` is established; the side-table accessors bypass the resolved-parent -gate, so detection sees the bindings directly. For each direct binding source of ce (parent plus -each ``implements`` entry), the check composes the ce-to-source binding with -``zend_get_inheritance_binding_full`` for every generic target reachable from source. Records are -stored in a transient ``HashTable`` keyed by target ``zend_class_entry *``; +Diamond detection is in ``zend_validate_generic_diamond_bindings``. It runs *before* +``ce->parent`` is set and ``ZEND_ACC_RESOLVED_PARENT`` is established; the side-table accessors +bypass the resolved-parent gate, so detection sees the bindings directly. For each direct binding +source of ce (parent plus each ``implements`` entry), the check composes the ce-to-source binding +with ``zend_get_inheritance_binding_full`` for every generic target reachable from source. +Records are stored in a transient ``HashTable`` keyed by target ``zend_class_entry *``; ``zend_diamond_record_or_check`` compares only arity. Differing arity is structurally inconsistent and fires ``zend_error_noreturn`` with both source paths. Differing args at matching -arity are admitted at this stage and resolved downstream - the interface-level merge synthesises +arity are admitted at this stage and resolved downstream — the interface-level merge synthesises a use-site-variance-aware contract on the inheriting interface, and per-path LSP verifies any concrete implementer against each substituted parent prototype. ``zend_check_generic_link_bounds`` validates each supplied arg against the corresponding target parameter's bound. When the supplied arg is a leaf class-scope ``T``-ref of ce, the effective arg -type is ce's own bound on that parameter - "ce's bound on Y must satisfy target's bound on T". An +type is ce's own bound on that parameter — "ce's bound on Y must satisfy target's bound on T". An unbounded child parameter cannot be forwarded into a bounded ancestor slot. +*********************** + Reflection visibility +*********************** + +Reflection is structured so that the *bound view* answers "what can a caller actually pass at +this slot" — the most common question — while the *reified shape* is reachable for tooling that +needs it: + +- ``ReflectionType::__toString()`` and ``ReflectionNamedType::getName()`` return the bound's + text (``Foo`` for ``T : Foo``, ``mixed`` for unbounded ``T``, ``Container`` for + ``Container``). This matches what an ordinary parameter / return / property slot enforces + at runtime. +- ``ReflectionNamedType::hasGenericArguments()`` / ``getGenericArguments()`` return the type + arguments of a generic application as ``ReflectionType`` instances in source order. +- ``ReflectionTypeParameterReference`` appears inside reified type expressions only — as an + element of ``getGenericArguments()``, as a bound, as a default, or as a nested arg. It is the + reflection type you see when a ``T``-ref shows up inside another type, not on a parameter + whose declared type *is* ``T``. +- ``ReflectionFunctionAbstract::getGenericParameters()`` and ``ReflectionClass::getGenericParameters()`` + return the entity's own declaration list. +- ``ReflectionClass::getGenericArgumentsFor{ParentClass,ParentInterface,UsedTrait}()`` returns + the args supplied at the class's own ``extends`` / ``implements`` / ``use`` clause. + ****************************** Opcache, optimizer, and JIT ****************************** @@ -238,4 +357,5 @@ in place would walk an unrelated frame. Under JIT, ``ZEND_VERIFY_GENERIC_ARGUMENTS`` falls through to the interpreter. The handler is cold, so the dispatch cost is dwarfed by the type-comparison work. Code that doesn't use turbofish doesn't emit the opcode, and the surrounding ``INIT_*`` and ``ZEND_NEW`` JIT -specializations are unaffected. +specializations are unaffected. The deferred ``instanceof`` / ``catch`` lookup likewise falls +through to ``zend_fetch_class`` like any other ``IS_UNUSED`` class operand. From e7a12e72fa4c58311a0155c07eb5a0fa23790cc7 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 20 May 2026 23:21:45 +0200 Subject: [PATCH 09/30] get opcache working-ish Signed-off-by: Robert Landers --- Zend/Optimizer/compact_literals.c | 12 + Zend/Optimizer/dfa_pass.c | 7 + Zend/Optimizer/optimize_func_calls.c | 118 ++- Zend/Optimizer/zend_optimizer.c | 140 +++ .../forwarded_t_ref_value_check.phpt | 87 ++ .../instanceof_with_t_ref_class_level.phpt | 3 - Zend/zend.c | 1 + Zend/zend_builtin_functions.c | 4 +- Zend/zend_compile.c | 416 +++++++- Zend/zend_compile.h | 66 +- Zend/zend_execute_API.c | 17 +- Zend/zend_globals.h | 8 + Zend/zend_inheritance.c | 94 +- Zend/zend_opcode.c | 55 +- Zend/zend_operators.c | 4 +- Zend/zend_vm_def.h | 112 ++- Zend/zend_vm_execute.h | 807 ++++++++++++--- Zend/zend_vm_handlers.h | 930 +++++++++--------- Zend/zend_vm_opcodes.c | 6 +- Zend/zend_vm_opcodes.h | 3 +- bench/bench_reification.php | 115 ++- ext/opcache/jit/zend_jit.c | 12 + ext/opcache/jit/zend_jit_ir.c | 23 + ext/opcache/jit/zend_jit_trace.c | 14 + ext/opcache/tests/opt/jmp_001.phpt | 9 +- ext/opcache/tests/pipe_optimizations.phpt | 5 +- ext/opcache/zend_file_cache.c | 121 ++- ext/opcache/zend_persist.c | 107 +- ext/opcache/zend_persist_calc.c | 44 +- 29 files changed, 2620 insertions(+), 720 deletions(-) create mode 100644 Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c index cf74dd8fc147..cf6f39f31019 100644 --- a/Zend/Optimizer/compact_literals.c +++ b/Zend/Optimizer/compact_literals.c @@ -674,6 +674,12 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx cache_size += sizeof(void *); class_slot[opline->op2.constant] = opline->extended_value; } + } else if (opline->opcode == ZEND_INSTANCEOF + && opline->op2_type == IS_UNUSED) { + /* instanceof T / instanceof Box: 3-slot PIC keyed on + * (type_args generation, called scope, resolved ce). */ + opline->extended_value = cache_size; + cache_size += 3 * sizeof(void *); } break; case ZEND_NEW: @@ -698,6 +704,12 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx cache_size += sizeof(void *); class_slot[opline->op1.constant] = opline->extended_value & ~ZEND_LAST_CATCH; } + } else { + ZEND_ASSERT(opline->op1_type == IS_UNUSED); + /* T-ref / deferred-turbofish catch: 3-slot PIC keyed on + * (type_args generation, called scope, resolved ce). */ + opline->extended_value = cache_size | (opline->extended_value & ZEND_LAST_CATCH); + cache_size += 3 * sizeof(void *); } break; case ZEND_BIND_GLOBAL: diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c index 77dc322fbdec..76ca8e5145f0 100644 --- a/Zend/Optimizer/dfa_pass.c +++ b/Zend/Optimizer/dfa_pass.c @@ -297,6 +297,13 @@ static inline bool can_elide_return_type_check( if (use_type & MAY_BE_REF) { return false; } + /* When the declared return type contains a generic parameter, the + * compile-time arg_info is the erased type (typically mixed) but the + * effective type on a monomorph is the substituted one — eliding the + * VERIFY_RETURN_TYPE here would skip the per-monomorph runtime check. */ + if (op_array->generic_types && op_array->generic_types->return_type) { + return false; + } if (use_type & MAY_BE_UNDEF) { use_type &= ~MAY_BE_UNDEF; diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c index bcbb8e07b913..9995473f48ab 100644 --- a/Zend/Optimizer/optimize_func_calls.c +++ b/Zend/Optimizer/optimize_func_calls.c @@ -74,6 +74,69 @@ static void zend_delete_call_instructions(const zend_op_array *op_array, zend_op } } +/* The compiler emits a speculative VERIFY_GENERIC_ARGUMENTS at call sites + * where the callee can't be resolved at compile time; the runtime handler + * short-circuits when the resolved callee turns out to be non-generic. Once + * the optimizer has resolved the callee here, that VERIFY (and any INSTALL, + * defensively) is provably dead — NOP it out so downstream passes (inline, + * DCE, JIT) don't have to model the extra opcode. + * + * For NEW the VERIFY also performs the class-level generic-arg arity/bound + * check against the *class*, not the constructor; in that case the + * constructor's func is irrelevant and we have to consult the class entry + * passed alongside it. */ +static void zend_nop_dead_generic_verify( + zend_op *do_opline, const zend_function *func, const zend_class_entry *new_ce) +{ + if (new_ce && new_ce->generic_parameters) { + return; /* `new GenericClass::<...>` VERIFY does class-arity check */ + } + if (func && ZEND_USER_CODE(func->common.type) && func->op_array.generic_parameters) { + return; /* genuinely generic — leave the verify alone */ + } + if (!func && !new_ce) return; + zend_op *opline = do_opline - 1; + int call = 0; + while (1) { + switch (opline->opcode) { + case ZEND_INIT_FCALL_BY_NAME: + case ZEND_INIT_NS_FCALL_BY_NAME: + case ZEND_INIT_STATIC_METHOD_CALL: + case ZEND_INIT_METHOD_CALL: + case ZEND_INIT_FCALL: + case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: + case ZEND_NEW: + case ZEND_INIT_DYNAMIC_CALL: + case ZEND_INIT_USER_CALL: + /* Hitting any call-setup at call==0 means we've reached the + * matching INIT/NEW for this DO_FCALL — no further VERIFY + * to scan; bail out before walking off the opcode array. */ + if (call == 0) return; + call--; + break; + case ZEND_DO_FCALL: + case ZEND_DO_ICALL: + case ZEND_DO_UCALL: + case ZEND_DO_FCALL_BY_NAME: + call++; + break; + case ZEND_VERIFY_GENERIC_ARGUMENTS: + case ZEND_INSTALL_GENERIC_ARGS: + if (call == 0) { + /* Even when the callee is non-generic, VERIFY's arity + * check needs to fire if turbofish args were supplied + * (`f::()` on a non-generic `f` must error). The + * compiler stores the turbofish args_id in extended_value + * — non-zero means "user passed turbofish here." */ + if (opline->extended_value != 0) return; + MAKE_NOP(opline); + } + break; + } + opline--; + } +} + /* Returns true if a VERIFY_GENERIC_ARGUMENTS sits between this call's INIT and * DO opcodes; such a call cannot be inlined because the verify opcode reads * EX(call), which goes away once the frame is dropped. */ @@ -88,13 +151,15 @@ static bool zend_call_has_generic_arguments_check(zend_op *opline) case ZEND_INIT_METHOD_CALL: case ZEND_INIT_FCALL: case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: - if (call == 0) { - return false; - } - ZEND_FALLTHROUGH; case ZEND_NEW: case ZEND_INIT_DYNAMIC_CALL: case ZEND_INIT_USER_CALL: + /* Same termination guard as zend_nop_dead_generic_verify + * — NEW also acts as the call setup for the constructor's + * DO_FCALL in `new Foo()` (no separate INIT). */ + if (call == 0) { + return false; + } call--; break; case ZEND_DO_FCALL: @@ -104,6 +169,7 @@ static bool zend_call_has_generic_arguments_check(zend_op *opline) call++; break; case ZEND_VERIFY_GENERIC_ARGUMENTS: + case ZEND_INSTALL_GENERIC_ARGS: if (call == 0) { return true; } @@ -137,10 +203,12 @@ static void zend_try_inline_call(zend_op_array *op_array, const zend_op *fcall, return; } - if (op_array->generic_types - && op_array->generic_types->turbofish_args - && zend_call_has_generic_arguments_check(opline - 1)) { - /* The verify opcode must run; inlining would orphan it. */ + if (zend_call_has_generic_arguments_check(opline - 1)) { + /* The verify opcode must run; inlining would orphan it. The + * scan is cheap and handles both the turbofish path (where + * generic_types->turbofish_args is populated) and the + * speculative emit for unknown-at-compile-time callees + * (which has no turbofish args at all). */ return; } @@ -239,6 +307,24 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx) case ZEND_DO_FCALL_BY_NAME: case ZEND_CALLABLE_CONVERT: call--; + /* INIT_DYNAMIC_CALL / INIT_USER_CALL don't get their callee + * resolved in the upper switch (no specialized INIT rewrite is + * possible), but we can still resolve for VERIFY-elision purposes + * — get_called_func now infers a callee when the CV holds a + * known constant or NEW result. Use the result for VERIFY + * elision only; don't promote the INIT/DO opcodes or attempt + * inlining, since the call site must stay dynamic-shaped. */ + if (!call_stack[call].func && call_stack[call].opline + && (call_stack[call].opline->opcode == ZEND_INIT_DYNAMIC_CALL + || call_stack[call].opline->opcode == ZEND_INIT_USER_CALL) + && opline->opcode != ZEND_CALLABLE_CONVERT) { + bool is_prototype = false; + const zend_function *func = zend_optimizer_get_called_func( + ctx->script, op_array, call_stack[call].opline, &is_prototype); + if (func) { + zend_nop_dead_generic_verify(opline, func, NULL); + } + } if (call_stack[call].func && call_stack[call].opline) { zend_op *fcall = call_stack[call].opline; @@ -274,6 +360,22 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx) opline->opcode = zend_get_call_op(fcall, call_stack[call].func, !RESULT_UNUSED(opline)); } + /* Strip the speculative generic-arg verify now that we + * know the callee — needs to happen before the inline + * attempt below, which bails out when a verify still + * sits between INIT and DO. For NEW, pass the class + * entry separately so the helper can spot a generic + * class with a non-generic constructor (the VERIFY's + * arity check is against the class, not the ctor). */ + if (opline->opcode != ZEND_CALLABLE_CONVERT) { + const zend_class_entry *new_ce = NULL; + if (fcall->opcode == ZEND_NEW) { + new_ce = zend_optimizer_get_class_entry_from_op1( + ctx->script, op_array, fcall); + } + zend_nop_dead_generic_verify(opline, call_stack[call].func, new_ce); + } + if ((ZEND_OPTIMIZER_PASS_16 & ctx->optimization_level) && call_stack[call].try_inline && opline->opcode != ZEND_CALLABLE_CONVERT) { diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index df472bed3040..5ffdefac49a3 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -902,6 +902,87 @@ const zend_class_constant *zend_fetch_class_const_info( return const_info; } +/* Returns the unique opcode in this op_array that writes to the given CV, or + * NULL if there are zero or multiple writers, or if any operation could mutate + * the CV through a reference. This is sound enough to use as a single-value + * assumption for CV-based callsite resolution: if we conclude a CV holds X at + * the call, eliding the runtime VERIFY is safe because any other actual value + * at runtime would cause the call itself to fail before VERIFY ever ran. */ +static const zend_op *zend_find_unique_cv_writer( + const zend_op_array *op_array, uint32_t cv_var) +{ + const zend_op *end = op_array->opcodes + op_array->last; + const zend_op *unique = NULL; + + for (const zend_op *op = op_array->opcodes; op < end; op++) { + if (op->result_type == IS_CV && op->result.var == cv_var) { + if (op->opcode == ZEND_QM_ASSIGN) { + if (unique) return NULL; + unique = op; + continue; + } + return NULL; + } + + switch (op->opcode) { + case ZEND_ASSIGN: + if (op->op1_type == IS_CV && op->op1.var == cv_var) { + if (unique) return NULL; + unique = op; + } + break; + case ZEND_ASSIGN_REF: + case ZEND_BIND_GLOBAL: + case ZEND_BIND_INIT_STATIC_OR_JMP: + case ZEND_BIND_STATIC: + case ZEND_SEND_REF: + case ZEND_SEND_VAR_NO_REF: + case ZEND_SEND_VAR_NO_REF_EX: + case ZEND_SEND_USER: + case ZEND_SEND_UNPACK: + case ZEND_SEND_ARRAY: + case ZEND_FE_FETCH_RW: + case ZEND_FE_RESET_RW: + case ZEND_ASSIGN_OP: + case ZEND_ASSIGN_DIM: + case ZEND_ASSIGN_OBJ: + case ZEND_ASSIGN_DIM_OP: + case ZEND_ASSIGN_OBJ_OP: + case ZEND_ASSIGN_OBJ_REF: + case ZEND_ASSIGN_STATIC_PROP_OP: + case ZEND_PRE_INC: + case ZEND_PRE_DEC: + case ZEND_POST_INC: + case ZEND_POST_DEC: + case ZEND_UNSET_CV: + case ZEND_FETCH_W: + case ZEND_FETCH_REF: + case ZEND_FETCH_FUNC_ARG: + if (op->op1_type == IS_CV && op->op1.var == cv_var) { + return NULL; + } + break; + } + } + + return unique; +} + +/* Walks backward from `from` to find the most recent opcode whose result port + * wrote to the given TMP/VAR slot. Used to chain `ASSIGN CV = TMP`-style + * patterns through to the value-producing opcode (e.g. NEW). */ +static const zend_op *zend_find_recent_tmp_writer( + const zend_op_array *op_array, const zend_op *from, uint32_t tmp_var) +{ + for (const zend_op *op = from - 1; op >= op_array->opcodes; op--) { + if ((op->result_type == IS_TMP_VAR || op->result_type == IS_VAR) + && op->result.var == tmp_var) { + return op; + } + } + return NULL; +} + zend_function *zend_optimizer_get_called_func( const zend_script *script, const zend_op_array *op_array, zend_op *opline, bool *is_prototype) { @@ -979,6 +1060,65 @@ zend_function *zend_optimizer_get_called_func( } return fbc; } + } else if (opline->op1_type == IS_CV + && opline->op2_type == IS_CONST && Z_TYPE_P(CRT_CONSTANT(opline->op2)) == IS_STRING) { + /* `$o->method()` where $o has a unique ASSIGN whose source is a + * NEW result. The instance class is then known exactly (not just + * a base class), so the resolved method isn't a prototype. */ + const zend_op *cv_writer = zend_find_unique_cv_writer(op_array, opline->op1.var); + if (cv_writer && cv_writer->opcode == ZEND_ASSIGN + && (cv_writer->op2_type == IS_TMP_VAR || cv_writer->op2_type == IS_VAR)) { + const zend_op *src = zend_find_recent_tmp_writer( + op_array, cv_writer, cv_writer->op2.var); + if (src && src->opcode == ZEND_NEW) { + const zend_class_entry *ce = zend_optimizer_get_class_entry_from_op1( + script, op_array, src); + if (ce && ce->type == ZEND_USER_CLASS) { + zend_string *method_name = Z_STR_P(CRT_CONSTANT(opline->op2) + 1); + zend_function *fbc = zend_hash_find_ptr(&ce->function_table, method_name); + if (fbc) { + uint32_t flags = fbc->common.fn_flags; + if (flags & ZEND_ACC_PRIVATE) { + if (fbc->common.scope != op_array->scope) return NULL; + } else if (flags & ZEND_ACC_PROTECTED) { + if (!op_array->scope + || !instanceof_function(op_array->scope, fbc->common.scope)) { + return NULL; + } + } + return fbc; + } + } + } + } + } + break; + case ZEND_INIT_DYNAMIC_CALL: + /* `$f(...)` where $f's unique writer is `ASSIGN $f = "name"`. The + * literal string isn't normalized at compile time the way an + * INIT_FCALL_BY_NAME literal is, so lowercase it for the lookup. */ + if (opline->op2_type == IS_CV) { + const zend_op *cv_writer = zend_find_unique_cv_writer(op_array, opline->op2.var); + if (cv_writer && cv_writer->opcode == ZEND_ASSIGN + && cv_writer->op2_type == IS_CONST) { + const zval *val = CRT_CONSTANT_EX(op_array, cv_writer, cv_writer->op2); + if (Z_TYPE_P(val) == IS_STRING && Z_STRLEN_P(val) > 0 + && Z_STRVAL_P(val)[0] != '\\') { + zend_string *lc_name = zend_string_tolower(Z_STR_P(val)); + zend_function *func; + zval *func_zv; + zend_function *result = NULL; + if (script && (func = zend_hash_find_ptr(&script->function_table, lc_name)) != NULL) { + result = func; + } else if ((func_zv = zend_hash_find(EG(function_table), lc_name)) != NULL) { + if (!zend_optimizer_ignore_function(func_zv, op_array->filename)) { + result = Z_PTR_P(func_zv); + } + } + zend_string_release_ex(lc_name, 0); + if (result) return result; + } + } } break; case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: { diff --git a/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt b/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt new file mode 100644 index 000000000000..904ee2998f7e --- /dev/null +++ b/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt @@ -0,0 +1,87 @@ +--TEST-- +Reification: forwarded T-ref in turbofish (id::(...) inside nested) resolves the value-arg check through the caller's bindings +--FILE-- +(T $x): T { return $x; } + +// Same-type forwarding: U binds to Dog at the outer call, the inner +// id::($x) forwards U into id's T, and id should accept Dog as Dog — +// not as a literal "U" type. +function nested(U $x): U { + return id::($x); +} +var_dump(nested::(new Dog)); + +// Mismatch caught against the *resolved* class, not the forwarded T-ref. +// The error message must name the bound class ("Dog"), not the parameter +// letter ("U"). +function pass_through(mixed $x): U { + return id::($x); +} +try { + pass_through::(new Cat); +} catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; +} + +// Forwarded scalar binding: U = int, the value 42 satisfies id's T = int. +function nested_scalar(U $x): U { + return id::($x); +} +var_dump(nested_scalar::(42)); + +// Forwarded scalar mismatch caught against the resolved scalar type: +// pass_through_scalar(mixed $x) doesn't constrain $x at its own +// boundary, so the string "hello" reaches the inner id::(...) call. +// id's T is resolved to int via U, and the value check fires with the +// resolved type ("int"), not the literal T-ref letter. +function pass_through_scalar(mixed $x): U { + return id::($x); +} +try { + pass_through_scalar::("hello"); +} catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; +} + +// Composite forwarded bindings: a union arg threaded through a forwarding +// wrapper still names the union ("int|string") in the error, not the +// T-ref letter. Same with intersection. +try { + pass_through_scalar::([1, 2]); +} catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; +} + +// Intersection forwarded through a wrapper: positive case (a value +// implementing both interfaces) passes; negative reports the resolved +// intersection ("Fooable&Barable"), not the T-ref letter. +interface Fooable {} +interface Barable {} +class Both implements Fooable, Barable {} +class FooOnly implements Fooable {} + +function pass_through_obj(object $x): U { + return id::($x); +} +var_dump(pass_through_obj::(new Both)); +try { + pass_through_obj::(new FooOnly); +} catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +object(Dog)#%d (0) { +} +caught: id(): Argument #1 ($x) must be of type Dog, Cat given +int(42) +caught: id(): Argument #1 ($x) must be of type int, string given +caught: id(): Argument #1 ($x) must be of type int|string, array given +object(Both)#%d (0) { +} +caught: id(): Argument #1 ($x) must be of type Fooable&Barable, FooOnly given diff --git a/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt b/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt index 6f7d1b17462c..21a3bda926df 100644 --- a/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt +++ b/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt @@ -1,6 +1,3 @@ - --TEST-- Reification: instanceof Box with class-level T resolves against the called scope's monomorph args --FILE-- diff --git a/Zend/zend.c b/Zend/zend.c index f16b1a30dbbc..5ba3b49e49fb 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -820,6 +820,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{ #endif executor_globals->saved_fpu_cw_ptr = NULL; executor_globals->active = false; + executor_globals->monomorph_synthesis_active = false; executor_globals->bailout = NULL; executor_globals->error_handling = EH_NORMAL; executor_globals->exception_class = NULL; diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 71d8cc7980fc..6d63aab29170 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -402,8 +402,8 @@ ZEND_FUNCTION(func_get_type_args) array_init_size(return_value, params->count); for (uint32_t i = 0; i < params->count; i++) { zval entry; - if (table && i < table->count && table->names[i]) { - ZVAL_STR_COPY(&entry, table->names[i]); + if (table && i < table->count && table->entries[i].name) { + ZVAL_STR_COPY(&entry, table->entries[i].name); } else { ZVAL_NULL(&entry); } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 94cbe4ca33c9..2e8736ba9108 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -591,6 +591,12 @@ static zend_always_inline void zend_compute_generic_required_total( static zend_generic_type_table *zend_generic_get_or_create_op_array_table(zend_op_array *op_array); ZEND_API const zend_type *zend_generic_get_turbofish_args(const zend_op_array *caller_op_array, uint32_t args_id) +{ + zend_turbofish_args_entry *entry = zend_generic_get_turbofish_call_entry(caller_op_array, args_id); + return entry ? &entry->args_box : NULL; +} + +ZEND_API zend_turbofish_args_entry *zend_generic_get_turbofish_call_entry(const zend_op_array *caller_op_array, uint32_t args_id) { if (!caller_op_array || !caller_op_array->generic_types || !caller_op_array->generic_types->turbofish_args || args_id == 0) { return NULL; @@ -637,6 +643,111 @@ static void zend_check_generic_argument_bounds( } } +/* Fingerprint the caller bindings that this args_box's T-refs resolve + * against, mixing in the interned-name pointers of each referenced caller + * entry. Used as the cache key on zend_turbofish_args_entry. Returns 0 when + * caching is unsafe (caller binding has only an owned_type — i.e., the + * type_ref would point into a frame-local zend_type that dies when the + * caller unwinds) or when there's no caller frame to read from. Concrete + * args_boxes (no T-refs) return the constant sentinel CONCRETE — the + * resulting table is invariant across calls and the cache slot acts as a + * once-built memo. */ +static uintptr_t zend_compute_call_cache_key( + const zend_type *args_box, const zend_execute_data *caller) +{ + if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return ZEND_TURBOFISH_CACHE_KEY_CONCRETE; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + uintptr_t fp = 0; + bool has_tref = false; + for (uint32_t i = 0; i < nwa->count; i++) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(nwa->args[i])) continue; + has_tref = true; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(nwa->args[i]); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) return 0; + if (!caller || !caller->type_args) return 0; + if (ref->index >= caller->type_args->count) return 0; + const zend_type_arg_entry *caller_entry = &caller->type_args->entries[ref->index]; + if (!caller_entry->name || !caller_entry->type_ref) return 0; + fp = fp * 0x9E3779B97F4A7C15ULL + (uintptr_t) caller_entry->name + i + 1; + } + if (!has_tref) { + return ZEND_TURBOFISH_CACHE_KEY_CONCRETE; + } + /* Steer clear of the sentinel values used to mean "empty" / "concrete". */ + if (fp == 0 || fp == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + fp = (uintptr_t) -1; + } + return fp; +} + +/* Returns true when the cache key fully determines the call's type_arg_table, + * i.e. no inference will fire that could plug an unset slot from a runtime + * value. Inference depends on argument classes — not part of the key — so a + * call that triggers it must be excluded from caching even if the + * caller-binding fingerprint matches. */ +static bool zend_call_is_cacheable_against_args( + const zend_function *fbc, const zend_type *args_box) +{ + if (!fbc || !ZEND_USER_CODE(fbc->common.type)) return false; + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params) return false; + uint64_t inferable = params->inferable_mask; + if (!inferable) return true; + uint32_t passed = 0; + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + passed = ZEND_TYPE_NAMED_WITH_ARGS(*args_box)->count; + } + for (uint32_t i = 0; i < params->count && i < 64; i++) { + if (!(inferable & ((uint64_t) 1 << i))) continue; + if (i < passed) continue; + if (ZEND_TYPE_IS_SET(params->parameters[i].default_type)) continue; + return false; + } + return true; +} + +/* Cache-aware wrapper: VERIFY/INSTALL handlers route through this instead of + * zend_build_generic_call_type_args directly. On hit, returns the cached + * shared table (already marked persisted, so frame teardown leaves it + * alone). On miss, builds normally; if the call site is single-binding + * stable, the freshly built table is also stashed in the cache for future + * iterations. The "store on first miss, never replace" policy avoids + * dangling: a replaced cached_table could still be referenced by a live + * frame's call->type_args. */ +ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( + zend_execute_data *call, zend_turbofish_args_entry *entry) +{ + if (!entry) { + return zend_build_generic_call_type_args(call, NULL); + } + const zend_type *args_box = &entry->args_box; + zend_execute_data *caller = EG(current_execute_data); + /* The entry lives in the caller op_array's generic_types side table; when + * that side table has been persisted to opcache SHM it's mapped read-only, + * so any cache writes here would SIGSEGV. The `persisted` flag on the + * generic_type_table is set by zend_persist_generic_type_table and is the + * authoritative signal (op_array->fn_flags ZEND_ACC_IMMUTABLE is unset on + * $_main even though its side table is still in SHM). */ + bool entry_writable = !caller || !caller->func + || !ZEND_USER_CODE(caller->func->common.type) + || !caller->func->op_array.generic_types + || !caller->func->op_array.generic_types->persisted; + uintptr_t key = zend_compute_call_cache_key(args_box, caller); + if (key && entry->cached_table && entry->cache_key == key) { + return entry->cached_table; + } + zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + if (t && key && entry_writable && !entry->cached_table + && zend_call_is_cacheable_against_args(call->func, args_box)) { + entry->cached_table = t; + entry->cache_key = key; + t->persisted = true; + } + return t; +} + /* Slots left NULL mean "fall back to the parameter's bound". Order of resolution * for each slot: explicit turbofish arg → parameter's declared default → * value-directed inference from any argument whose pre-erasure type is a @@ -666,29 +777,37 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( zend_execute_data *caller = EG(current_execute_data); for (uint32_t i = 0; i < params->count; i++) { - zend_type src = ZEND_TYPE_INIT_NONE(0); + const zend_type *src = NULL; if (i < passed) { - src = passed_args[i]; + src = &passed_args[i]; } else if (ZEND_TYPE_IS_SET(params->parameters[i].default_type)) { - src = params->parameters[i].default_type; + src = ¶ms->parameters[i].default_type; } - if (!ZEND_TYPE_IS_SET(src)) { + if (!src || !ZEND_TYPE_IS_SET(*src)) { continue; } /* A turbofish arg can itself name an outer generic parameter (e.g. * `inner::(...)` inside `function outer`). Resolve those - * against the caller frame's T-table so the binding chains. */ - if (ZEND_TYPE_HAS_TYPE_PARAMETER(src)) { - const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(src); + * against the caller frame's T-table so the binding chains — + * borrow whatever the caller's entry resolved to (which is itself + * either another borrowed ref or an owned synthesised type), and + * copy the canonical name. Both source lifetimes outlive this + * frame because frames nest. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*src)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*src); if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE && caller && caller->type_args && ref->index < caller->type_args->count - && caller->type_args->names[ref->index]) { - table->names[i] = zend_string_copy(caller->type_args->names[ref->index]); + && caller->type_args->entries[ref->index].name) { + table->entries[i].name = zend_string_copy(caller->type_args->entries[ref->index].name); + table->entries[i].type_ref = zend_type_arg_entry_type(&caller->type_args->entries[ref->index]); } continue; } - table->names[i] = zend_type_arg_canonical_name(src); + table->entries[i].name = zend_type_arg_canonical_name(*src); + /* Borrow the args_box's slot — args_box lives on a caller-chain + * op_array and outlives this frame's type_args. */ + table->entries[i].type_ref = src; } /* Inference for unset slots. Composite shapes (array, Box, ?T) are @@ -696,7 +815,7 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( * inferable_mask short-circuits when no inferable slot remains unset. */ uint64_t unbound_inferable = params->inferable_mask; for (uint32_t i = 0; i < params->count && i < 64; i++) { - if (table->names[i]) { + if (table->entries[i].name) { unbound_inferable &= ~((uint64_t) 1 << i); } } @@ -715,10 +834,17 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; if (ref->index >= table->count) continue; - if (table->names[ref->index]) continue; + if (table->entries[ref->index].name) continue; zval *arg = ZEND_CALL_ARG(call, (uint32_t) arg_idx + 1); if (Z_TYPE_P(arg) == IS_OBJECT) { - table->names[ref->index] = zend_string_copy(Z_OBJCE_P(arg)->name); + zend_class_entry *arg_ce = Z_OBJCE_P(arg); + table->entries[ref->index].name = zend_string_copy(arg_ce->name); + /* No pre-existing zend_type to borrow — synthesise a + * class-name owned_type from the value's class so value-arg + * checks on forwarded T-refs compare instanceof correctly. */ + zend_string *inferred_name = zend_string_copy(arg_ce->name); + table->entries[ref->index].owned_type = + (zend_type) ZEND_TYPE_INIT_CLASS(inferred_name, 0, 0); } } ZEND_HASH_FOREACH_END(); } @@ -726,6 +852,76 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( return table; } +/* Recursive value check for a pre-erasure zend_type. Pre-erasure stores + * unions / intersections as a LIST with CODE-only / NAME / NAMED_WITH_ARGS + * members (one per branch), which is not the canonical shape that + * zend_check_type_slow understands — it expects scalar bits on the outer + * mask. Walk the shape directly here: + * + * - LIST with INTERSECTION_BIT: all members must match (object only). + * - LIST (union): any member matches. + * - NAMED_WITH_ARGS: instanceof against the canonical monomorph class; + * null also accepted if MAY_BE_NULL is set on the type's mask. + * - NAME: instanceof against the class; same null short-circuit. + * - Otherwise (CODE-only mask): scalar-hint check + null short-circuit. + * + * Returns true if the value satisfies the type. The `strict` argument + * controls scalar coercion at the leaf. */ +static bool zend_check_pre_erasure_type_value( + const zend_type *type, zval *arg, const zend_reference *ref, bool strict) +{ + uint32_t mask = ZEND_TYPE_FULL_MASK(*type); + + if (ZEND_TYPE_IS_INTERSECTION(*type)) { + if (Z_TYPE_P(arg) != IS_OBJECT) { + return (mask & MAY_BE_NULL) && Z_TYPE_P(arg) == IS_NULL; + } + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(*type), member) { + if (!zend_check_pre_erasure_type_value(member, arg, NULL, strict)) { + return false; + } + } ZEND_TYPE_LIST_FOREACH_END(); + return true; + } + + if (ZEND_TYPE_HAS_LIST(*type)) { + if ((mask & MAY_BE_NULL) && Z_TYPE_P(arg) == IS_NULL) { + return true; + } + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(*type), member) { + if (zend_check_pre_erasure_type_value(member, arg, ref, strict)) { + return true; + } + } ZEND_TYPE_LIST_FOREACH_END(); + return false; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + if ((mask & MAY_BE_NULL) && Z_TYPE_P(arg) == IS_NULL) return true; + if (Z_TYPE_P(arg) != IS_OBJECT) return false; + zend_string *canonical = zend_type_to_canonical_string(*type); + zend_class_entry *ce = zend_lookup_class_ex(canonical, NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + zend_string_release(canonical); + return ce && instanceof_function(Z_OBJCE_P(arg), ce); + } + + if (ZEND_TYPE_HAS_NAME(*type)) { + if ((mask & MAY_BE_NULL) && Z_TYPE_P(arg) == IS_NULL) return true; + if (Z_TYPE_P(arg) != IS_OBJECT) return false; + zend_class_entry *ce = zend_lookup_class_ex(ZEND_TYPE_NAME(*type), NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + return ce && instanceof_function(Z_OBJCE_P(arg), ce); + } + + /* Mask-only — single scalar (e.g. INT) or a combined scalar mask. */ + if (ZEND_TYPE_CONTAINS_CODE(*type, Z_TYPE_P(arg))) return true; + if (ref && ZEND_REF_HAS_TYPE_SOURCES(ref)) return false; + return zend_verify_scalar_type_hint(mask, arg, strict, /* is_internal */ false); +} + /* Run PHP's normal arg-type machinery (coerce in weak mode, throw TypeError * in strict mode) against the per-call substituted type for any value-parameter * whose pre-erasure type is a direct TYPE_PARAMETER reference. Returns false @@ -743,12 +939,6 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ || !fbc->op_array.generic_types->parameters) { return true; } - /* Strict-types is determined by the caller's own declare; that's the - * function whose VERIFY opcode is currently running. */ - zend_execute_data *caller_ed = EG(current_execute_data); - bool strict = caller_ed - && caller_ed->func - && (caller_ed->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); const HashTable *pre = fbc->op_array.generic_types->parameters; uint32_t num_args = ZEND_CALL_NUM_ARGS(call); @@ -769,20 +959,34 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ zref = Z_REF_P(target); target = Z_REFVAL_P(target); } - if (ZEND_TYPE_CONTAINS_CODE(substituted, Z_TYPE_P(target))) { - continue; - } - bool ok; - if (ZEND_TYPE_HAS_NAME(substituted) && Z_TYPE_P(target) == IS_OBJECT) { - zend_class_entry *ce = zend_lookup_class_ex(ZEND_TYPE_NAME(substituted), NULL, - ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); - ok = ce && instanceof_function(Z_OBJCE_P(target), ce); - } else if (zref && ZEND_REF_HAS_TYPE_SOURCES(zref)) { - ok = false; - } else { - uint32_t type_mask = ZEND_TYPE_FULL_MASK(substituted); - ok = zend_verify_scalar_type_hint(type_mask, target, strict, /* is_internal_arg */ false); + + /* If the turbofish arg is itself a forwarded TYPE_PARAMETER (e.g. + * `id::($x)` inside `nested`), the caller's binding has + * already been resolved into call->type_args by + * zend_build_generic_call_type_args. Substitute the resolved + * zend_type in so downstream class/scalar/composite checks run + * against the actual bound type rather than the literal T-ref. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(substituted)) { + if (!call->type_args || ref->index >= call->type_args->count) { + continue; + } + const zend_type *resolved = zend_type_arg_entry_type( + &call->type_args->entries[ref->index]); + if (!resolved) { + /* No binding available; the erased parameter type (the + * parameter's bound) already accepted the value. */ + continue; + } + substituted = *resolved; } + + /* Pre-erasure shapes don't match the canonical PHP type layout + * (scalars sit as CODE-only list members instead of bits on the + * outer mask), so walk them directly. */ + bool strict = EG(current_execute_data) + && EG(current_execute_data)->func + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); + bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); if (!ok) { zend_string *expected = zend_type_to_string(substituted); const zend_arg_info *ai = (arg_idx < fbc->common.num_args) @@ -903,10 +1107,58 @@ static zend_type zend_compile_turbofish_args_type(zend_ast *turbofish_ast) return result; } +/* Returns true when every supplied turbofish arg is concrete (no T-refs) + * and `zend_check_generic_arg_satisfies_bound` resolves to + * INHERITANCE_SUCCESS — i.e. the runtime arity+bound check is redundant + * and we can emit ZEND_INSTALL_GENERIC_ARGS in place of + * ZEND_VERIFY_GENERIC_ARGUMENTS. Returns false on T-refs, on a definite + * INHERITANCE_ERROR (so the runtime VERIFY can throw the catchable + * TypeError as before), or on INHERITANCE_UNRESOLVED (a class isn't + * loaded yet — the runtime retry may succeed). */ +static bool zend_static_check_generic_call_bounds( + const zend_generic_parameter_list *params, + const zend_type_named_with_args *args, uint32_t arity) +{ + uint32_t check_count = arity < args->count ? arity : args->count; + if (check_count > params->count) check_count = params->count; + for (uint32_t i = 0; i < check_count; i++) { + if (zend_type_contains_type_parameter(args->args[i])) { + return false; + } + zend_type bound = params->parameters[i].bound; + if (!ZEND_TYPE_IS_SET(bound)) continue; + zend_inheritance_status status = + zend_check_generic_arg_satisfies_bound(NULL, args->args[i], NULL, bound); + if (status != INHERITANCE_SUCCESS) { + return false; + } + } + return true; +} + +/* Can the runtime VERIFY be replaced with the cheaper INSTALL at this CALL + * site? Requires the callee to be statically known and generic, the arity + * to be within [required, total], and the turbofish args to be concrete + * and bound-satisfying. */ +static bool zend_can_install_call_args_statically( + const zend_function *fbc, const zend_type *args_box, uint32_t arity) +{ + if (!fbc || !ZEND_USER_CODE(fbc->common.type)) return false; + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params) return false; + uint32_t required, total; + zend_compute_generic_required_total(params, &required, &total); + if (arity < required || arity > total) return false; + if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) return false; + return zend_static_check_generic_call_bounds( + params, ZEND_TYPE_NAMED_WITH_ARGS(*args_box), arity); +} + static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc) { uint32_t arity = 0; uint32_t args_id = 0; + const zend_type *args_box = NULL; if (turbofish_ast) { arity = zend_ast_get_list(turbofish_ast)->children; @@ -917,6 +1169,7 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t zend_generic_get_or_create_op_array_table(CG(active_op_array)); args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; zend_generic_type_table_set_turbofish_args(table, args_id, args_type); + args_box = zend_hash_index_find_ptr(table->turbofish_args, args_id); } else { /* No turbofish: only call sites get a defaults/inference-driven VERIFY * (NEW already emitted it above the args). Skip when fbc is known and @@ -931,8 +1184,17 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t } } + /* Replace the runtime VERIFY with INSTALL when arity+bound check is + * decidable (and passes) at compile time. Only CALL is handled for + * now — NEW's compile-time ce resolution path is more constrained. */ + uint8_t opcode = ZEND_VERIFY_GENERIC_ARGUMENTS; + if (kind == ZEND_VERIFY_ARITY_KIND_CALL + && zend_can_install_call_args_statically(fbc, args_box, arity)) { + opcode = ZEND_INSTALL_GENERIC_ARGS; + } + zend_op *opline = get_next_op(); - opline->opcode = ZEND_VERIFY_GENERIC_ARGUMENTS; + opline->opcode = opcode; opline->op2_type = IS_UNUSED; opline->op2.num = arity; opline->extended_value = args_id; @@ -2253,6 +2515,19 @@ static zend_string *resolve_class_name(zend_string *name, const zend_class_entry return zend_string_copy(name); } +/* Render a single union / intersection list member. The list iteration used + * to assert NAME on every member, but reified types put scalar codes, + * TYPE_PARAMETER refs, and NAMED_WITH_ARGS payloads in list slots too — + * defer to the main renderer which already handles each case. The outer + * union absorbs nullability with a single trailing `|null`, so strip + * MAY_BE_NULL from the member's own mask to avoid emitting `?Member`. */ +static zend_string *render_list_member(const zend_type *member, const zend_class_entry *scope) +{ + zend_type clean = *member; + ZEND_TYPE_FULL_MASK(clean) &= ~MAY_BE_NULL; + return zend_type_to_string_resolved(clean, scope); +} + static zend_string *add_intersection_type(zend_string *str, const zend_type_list *intersection_type_list, bool is_bracketed) @@ -2262,9 +2537,11 @@ static zend_string *add_intersection_type(zend_string *str, ZEND_TYPE_LIST_FOREACH(intersection_type_list, single_type) { ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*single_type)); - ZEND_ASSERT(ZEND_TYPE_HAS_NAME(*single_type)); - - intersection_str = add_type_string(intersection_str, ZEND_TYPE_NAME(*single_type), /* is_intersection */ true); + zend_string *piece = render_list_member(single_type, NULL); + if (piece) { + intersection_str = add_type_string(intersection_str, piece, /* is_intersection */ true); + zend_string_release(piece); + } } ZEND_TYPE_LIST_FOREACH_END(); ZEND_ASSERT(intersection_str); @@ -2295,12 +2572,11 @@ zend_string *zend_type_to_string_resolved(const zend_type type, const zend_class continue; } ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); - ZEND_ASSERT(ZEND_TYPE_HAS_NAME(*list_type)); - - zend_string *name = ZEND_TYPE_NAME(*list_type); - zend_string *resolved = resolve_class_name(name, scope); - str = add_type_string(str, resolved, /* is_intersection */ false); - zend_string_release(resolved); + zend_string *piece = render_list_member(list_type, scope); + if (piece) { + str = add_type_string(str, piece, /* is_intersection */ false); + zend_string_release(piece); + } } ZEND_TYPE_LIST_FOREACH_END(); } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); @@ -2576,7 +2852,11 @@ static zend_string *zend_canonical_one(zend_type type) ZEND_API zend_string *zend_type_to_canonical_string(zend_type type) { - return zend_canonical_one(type); + /* Intern the result so equivalent type-arg lists share storage and the + * downstream class-table lookup (zend_fetch_class_by_name) hashes the + * same pointer each time. Safe at runtime — zend_new_interned_string + * falls back to a heap string if interning isn't available. */ + return zend_new_interned_string(zend_canonical_one(type)); } ZEND_API bool zend_type_contains_type_parameter(zend_type type) @@ -2611,7 +2891,9 @@ ZEND_API zend_string *zend_generic_canonical_class_name( } smart_str_appendc(&buf, '>'); smart_str_0(&buf); - return buf.s; + /* Intern so repeat lookups of the same monomorph name share storage and + * EG(class_table) can dispatch on pointer identity. */ + return zend_new_interned_string(buf.s); } static bool is_generator_compatible_class_type(const zend_string *name) { @@ -3974,6 +4256,31 @@ static inline void zend_set_class_name_op1(zend_op *opline, znode *class_node) / } /* }}} */ +/* At a compile-time canonical-name production site, try to register the + * monomorph in the class table now so opcache picks it up via the regular + * CG(class_table) → script.class_table persistence path. If the base isn't + * compiled yet (forward reference, autoloaded class, etc.) or synthesis + * fails for any other reason, the runtime lookup hook synthesises lazily + * on first reference. + * + * Exceptions raised by synthesis (e.g. a bound violation in the monomorph + * being created) are suppressed here — the runtime path will rethrow them + * as a catchable TypeError exactly where it would have without this + * optimisation. */ +static void zend_try_compile_time_synthesize_monomorph(zend_string *canonical) +{ + zend_object *saved_exception = EG(exception); + EG(exception) = NULL; + + (void) zend_try_synthesize_monomorph_by_name(canonical, ZEND_FETCH_CLASS_NO_AUTOLOAD); + + if (EG(exception)) { + OBJ_RELEASE(EG(exception)); + EG(exception) = NULL; + } + EG(exception) = saved_exception; +} + static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t fetch_flags) /* {{{ */ { uint32_t fetch_type; @@ -3988,8 +4295,10 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f if (name_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { zend_type ty = zend_compile_pre_erasure_typename(name_ast); if (!zend_type_contains_type_parameter(ty)) { + zend_string *canonical = zend_type_to_canonical_string(ty); + zend_try_compile_time_synthesize_monomorph(canonical); result->op_type = IS_CONST; - ZVAL_STR(&result->u.constant, zend_type_to_canonical_string(ty)); + ZVAL_STR(&result->u.constant, canonical); zend_type_release(ty, /* persistent */ false); return; } @@ -6962,6 +7271,7 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ if (ok) { zend_string *canonical = zend_generic_canonical_class_name(ce->name, chosen, count); + zend_try_compile_time_synthesize_monomorph(canonical); opline->op1_type = IS_CONST; opline->op1.constant = zend_add_class_name_literal(canonical); opline->op2.num = zend_alloc_cache_slot(); @@ -8363,7 +8673,8 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ /* catch (T $e): resolve T at exception-throw time via the runtime * T-table. Encoded the same way as ZEND_FETCH_CLASS — see - * zend_resolve_generic_type_param. */ + * zend_resolve_generic_type_param. Allocate 3 cache slots for the + * polymorphic inline cache (see zend_vm_def.h ZEND_CATCH handler). */ bool emitted_type_param = false; if (class_ast->kind == ZEND_AST_ZVAL && (class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { @@ -8375,7 +8686,7 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ opline->op1_type = IS_UNUSED; opline->op1.num = zend_pack_type_param_fetch(param_index, 0, origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE); - opline->extended_value = 0; + opline->extended_value = zend_alloc_cache_slots(3); emitted_type_param = true; } } @@ -8390,6 +8701,7 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ zend_type ty = zend_compile_pre_erasure_typename(class_ast); if (!zend_type_contains_type_parameter(ty)) { resolved_name = zend_type_to_canonical_string(ty); + zend_try_compile_time_synthesize_monomorph(resolved_name); zend_type_release(ty, /* persistent */ false); } else { zend_generic_type_table *table = @@ -8399,7 +8711,7 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ zend_generic_type_table_set_turbofish_args(table, args_id, ty); opline->op1_type = IS_UNUSED; opline->op1.num = zend_pack_generic_deferred_fetch(args_id, 0); - opline->extended_value = 0; + opline->extended_value = zend_alloc_cache_slots(3); emitted_type_param = true; } } @@ -12661,6 +12973,14 @@ static void zend_compile_instanceof(znode *result, zend_ast *ast) /* {{{ */ opline->extended_value = zend_alloc_cache_slot(); } else { SET_NODE(opline->op2, &class_node); + /* Generic / T-ref class refs resolve through zend_fetch_class against + * the current frame's bindings, which are stable within a frame but + * vary across frames. Allocate a 3-slot polymorphic inline cache — + * keys (EX(type_args), called_scope), value resolved CE — so a hot + * loop inside one frame skips the resolver after the first hit. */ + if (class_node.op_type == IS_UNUSED) { + opline->extended_value = zend_alloc_cache_slots(3); + } } } /* }}} */ diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 8697c5c520ef..12b6ae61dc20 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -197,23 +197,77 @@ ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast); ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box); ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint32_t arity, const zend_type *args_box); ZEND_API const zend_type *zend_generic_get_turbofish_args(const zend_op_array *caller_op_array, uint32_t args_id); +ZEND_API struct _zend_turbofish_args_entry *zend_generic_get_turbofish_call_entry(const zend_op_array *caller_op_array, uint32_t args_id); + +/* Per-call-frame mapping of generic type parameters to their bindings. + * `name` is the canonical class name (or scalar/composite canonical string) + * for the bound type, used for class lookups and equality. The bound type + * itself is reachable via: + * - `type_ref`: a borrowed pointer to a zend_type in longer-lived + * storage — typically a turbofish args_box (held by some op_array's + * side table) or a parent frame's entry. Frames nest, so the source + * is guaranteed to outlive the entry. + * - `owned_type`: a zend_type owned by this entry. Used when no + * pre-existing zend_type can be borrowed — currently only for + * value-directed inference, where the inferred class-name zend_type + * is synthesised fresh. Released by zend_type_arg_table_destroy. + * Reads prefer `type_ref` when non-NULL, otherwise `owned_type`. When both + * are unset the parameter is unbound — consumers fall back to the + * parameter's declared bound. + * + * Lifetime: allocated by VERIFY_GENERIC_ARGUMENTS for a function call (or + * inferred at RECV time), freed when the frame unwinds. `generation` is a + * monotonically-increasing nonce assigned at allocation time so cache + * slots that key off the table pointer can detect ABA reuse. */ +typedef struct _zend_type_arg_entry { + zend_string *name; + const zend_type *type_ref; + zend_type owned_type; +} zend_type_arg_entry; -/* Per-call-frame mapping of generic type parameters to bound class names. NULL - * entries mean "use the parameter's bound" (the erased default). Lifetime is - * tied to the call frame: allocated by VERIFY_GENERIC_ARGUMENTS for a function - * call (or inferred at RECV time), freed when the frame unwinds. */ typedef struct _zend_type_arg_table { uint32_t count; - zend_string *names[1]; + uint32_t generation; + /* True when the table has been relocated into opcache SHM / file cache + * as part of a persisted class entry's `generic_type_args`. Tells the + * destroy path to leave the table and its name/type contents alone. */ + bool persisted; + zend_type_arg_entry entries[1]; } zend_type_arg_table; #define ZEND_TYPE_ARG_TABLE_SIZE(count) \ - (sizeof(zend_type_arg_table) + ((count) - 1) * sizeof(zend_string *)) + (sizeof(zend_type_arg_table) + ((count) - 1) * sizeof(zend_type_arg_entry)) + +/* Read the bound type for an entry — returns NULL if unbound. */ +static zend_always_inline const zend_type *zend_type_arg_entry_type(const zend_type_arg_entry *entry) { + if (entry->type_ref) return entry->type_ref; + if (ZEND_TYPE_IS_SET(entry->owned_type)) return &entry->owned_type; + return NULL; +} + +/* Per-call-site side entry: stores both the compile-time args_box and a + * one-slot runtime cache of the most recently built type-arg table at this + * call site, keyed on the caller frame's binding identities. On a hit, the + * cached table is reused (marked persisted so the frame doesn't free it) + * instead of rebuilding via zend_build_generic_call_type_args. Inline + * args_box keeps zend_generic_get_turbofish_args returning a stable pointer + * at the same offset that callers expect. */ +typedef struct _zend_turbofish_args_entry { + zend_type args_box; + zend_type_arg_table *cached_table; + /* 0 = empty; ZEND_TURBOFISH_CACHE_KEY_CONCRETE = invariant (no T-refs); + * otherwise a fingerprint built from the caller's referenced binding name + * pointers (interned, so pointer-stable across calls with same value). */ + uintptr_t cache_key; +} zend_turbofish_args_entry; + +#define ZEND_TURBOFISH_CACHE_KEY_CONCRETE ((uintptr_t)1) ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count); ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); +ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args(zend_execute_data *call, struct _zend_turbofish_args_entry *entry); ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type); ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, uint32_t fetch_type); ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index c281f82fe29a..b3cb93e81400 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1764,19 +1764,19 @@ ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, params = ex->func->op_array.generic_parameters; } - if (table && param_index < table->count && table->names[param_index]) { + if (table && param_index < table->count && table->entries[param_index].name) { /* Try the bound binding as a class name. If it isn't a class (e.g. * scalar T-arg like `int`) the lookup returns NULL silently and we * fall through to the parameter's declared bound — which is the only * remaining thing a bare T-ref can resolve to. */ uint32_t silent_fetch = (fetch_type & ~ZEND_FETCH_CLASS_EXCEPTION) | ZEND_FETCH_CLASS_SILENT | ZEND_FETCH_CLASS_NO_AUTOLOAD; - zend_class_entry *ce = zend_fetch_class_by_name(table->names[param_index], NULL, silent_fetch); + zend_class_entry *ce = zend_fetch_class_by_name(table->entries[param_index].name, NULL, silent_fetch); if (ce) return ce; /* Not in the class table yet — retry with autoload before giving up, * but only if the original fetch allowed it. */ if (!(fetch_type & ZEND_FETCH_CLASS_NO_AUTOLOAD)) { - ce = zend_fetch_class_by_name(table->names[param_index], NULL, + ce = zend_fetch_class_by_name(table->entries[param_index].name, NULL, (fetch_type & ~ZEND_FETCH_CLASS_EXCEPTION) | ZEND_FETCH_CLASS_SILENT); if (ce) return ce; } @@ -1818,8 +1818,8 @@ static zend_string *zend_resolve_fn_type_param_name(uint32_t index) zend_execute_data *ex = EG(current_execute_data); if (!ex || !ZEND_USER_CODE(ex->func->type)) return NULL; zend_type_arg_table *table = ex->type_args; - if (table && index < table->count && table->names[index]) { - return table->names[index]; + if (table && index < table->count && table->entries[index].name) { + return table->entries[index].name; } return NULL; } @@ -1835,7 +1835,7 @@ static zend_string *zend_resolve_class_type_param_name(uint32_t index) cur = cur->parent; } if (cur && cur->generic_type_args && index < cur->generic_type_args->count) { - return cur->generic_type_args->names[index]; + return cur->generic_type_args->entries[index].name; } return NULL; } @@ -1933,7 +1933,10 @@ ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, return NULL; } smart_str_0(&buf); - zend_string *canonical = buf.s; + /* Intern the canonical name so EG(class_table) lookups can dispatch on + * pointer identity and repeat executions of the same opline with the + * same bindings hit a stable, shared string. */ + zend_string *canonical = zend_new_interned_string(buf.s); zend_class_entry *ce = zend_fetch_class_by_name(canonical, NULL, fetch_type); zend_string_release(canonical); return ce; diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 421daeac8c3c..4ae7660c42f8 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -225,6 +225,14 @@ struct _zend_executor_globals { struct _zend_execute_data *current_execute_data; const zend_class_entry *fake_scope; /* used to avoid checks accessing properties */ + /* Set while a monomorph (e.g. Box) is being linked against its base. + * Bypasses the FINAL/READONLY checks the linker would otherwise raise + * on a user `extends FinalClass`, since monomorph synthesis is engine- + * internal and the resulting monomorph re-acquires the suppressed bits. + * Replaces a previous design that mutated base->ce_flags in place, which + * would SIGSEGV when base lived in read-only opcache SHM. */ + bool monomorph_synthesis_active; + uint32_t jit_trace_num; /* Used by tracing JIT to reference the currently running trace */ zend_execute_data *current_observed_frame; diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 6267225a548a..2aa55ee5d4f1 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2768,6 +2768,12 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par } } + /* Monomorph synthesis (Box extending Box) is engine-internal and + * needs to bypass FINAL / READONLY-mismatch errors that would block a + * user `extends FinalClass`. The synthesized child re-acquires those + * bits after linking via the propagated/suppressed logic in + * zend_synthesize_monomorph. */ + bool mono_link = EG(monomorph_synthesis_active); if (UNEXPECTED(ce->ce_flags & ZEND_ACC_INTERFACE)) { /* Interface can only inherit other interfaces */ if (UNEXPECTED(!(parent_ce->ce_flags & ZEND_ACC_INTERFACE))) { @@ -2780,7 +2786,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par zend_error_noreturn(E_COMPILE_ERROR, "Class %s cannot extend enum %s", ZSTR_VAL(ce->name), ZSTR_VAL(parent_ce->name)); } /* Class must not extend a final class */ - if (parent_ce->ce_flags & ZEND_ACC_FINAL) { + if (!mono_link && (parent_ce->ce_flags & ZEND_ACC_FINAL)) { zend_error_noreturn(E_COMPILE_ERROR, "Class %s cannot extend final class %s", ZSTR_VAL(ce->name), ZSTR_VAL(parent_ce->name)); } @@ -2792,7 +2798,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par } } - if (UNEXPECTED((ce->ce_flags & ZEND_ACC_READONLY_CLASS) != (parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS))) { + if (!mono_link && UNEXPECTED((ce->ce_flags & ZEND_ACC_READONLY_CLASS) != (parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS))) { zend_error_noreturn(E_COMPILE_ERROR, "%s class %s cannot extend %s class %s", ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "Readonly" : "Non-readonly", ZSTR_VAL(ce->name), parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "readonly" : "non-readonly", ZSTR_VAL(parent_ce->name) @@ -5596,11 +5602,54 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string uint32_t i, j; zval *zv; zend_string *synthesized_lc_parent = NULL; + zend_class_entry *cache_key_proto = NULL; ALLOCA_FLAG(use_heap) SET_ALLOCA_FLAG(use_heap); ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_LINKED)); + /* When this link will rewrite ce->parent_name or ce->interface_names + * during monomorph synthesis (extends/implements/use with concrete + * generic args), the writes happen below — but ce may live in read-only + * opcache SHM under opcache.protect_memory=1. Lazy-load the mutable copy + * now and remember the immutable pointer so the inheritance cache lookup + * further down still keys against the same proto opcache stored under. + * Limited to classes that actually carry a generic side-table so we don't + * pay the copy cost on every link. */ + if ((ce->ce_flags & ZEND_ACC_IMMUTABLE) + && ce->generic_types + && (ce->generic_types->extends + || ce->generic_types->implements + || ce->generic_types->trait_uses)) { + cache_key_proto = ce; + ce = zend_lazy_class_load(ce); + zv = zend_hash_find_known_hash(CG(class_table), key); + Z_CE_P(zv) = ce; + /* zend_lazy_class_load deep-copies properties/methods/etc. but the + * interface_names/trait_names arrays themselves still point into the + * immutable SHM. Monomorph synthesis below rewrites entries in + * those arrays, so detach them into emalloc'd copies first. The + * name strings are interned, so addref/release stays a no-op. */ + if (ce->num_interfaces) { + zend_class_name *src = ce->interface_names; + ce->interface_names = emalloc(sizeof(zend_class_name) * ce->num_interfaces); + memcpy(ce->interface_names, src, sizeof(zend_class_name) * ce->num_interfaces); + for (uint32_t k = 0; k < ce->num_interfaces; k++) { + zend_string_addref(ce->interface_names[k].name); + zend_string_addref(ce->interface_names[k].lc_name); + } + } + if (ce->num_traits) { + zend_class_name *src = ce->trait_names; + ce->trait_names = emalloc(sizeof(zend_class_name) * ce->num_traits); + memcpy(ce->trait_names, src, sizeof(zend_class_name) * ce->num_traits); + for (uint32_t k = 0; k < ce->num_traits; k++) { + zend_string_addref(ce->trait_names[k].name); + zend_string_addref(ce->trait_names[k].lc_name); + } + } + } + /* Bound-erased generics, extends-with-args: when this class declared * `extends Box`, its compile-time side table holds the pre-erasure * `Box` payload while `parent_name` is the bare base name. Before @@ -5885,9 +5934,12 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } #endif - if (ce->ce_flags & ZEND_ACC_IMMUTABLE && is_cacheable) { + if ((ce->ce_flags & ZEND_ACC_IMMUTABLE || cache_key_proto) && is_cacheable) { if (zend_inheritance_cache_get && zend_inheritance_cache_add) { - zend_class_entry *ret = zend_inheritance_cache_get(ce, parent, traits_and_interfaces); + /* When we lazy-loaded early (cache_key_proto != NULL), the + * immutable original is the cache key opcache stored under. */ + zend_class_entry *key_ce = cache_key_proto ? cache_key_proto : ce; + zend_class_entry *ret = zend_inheritance_cache_get(key_ce, parent, traits_and_interfaces); if (ret) { if (traits_and_interfaces) { free_alloca(traits_and_interfaces, use_heap); @@ -5899,7 +5951,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } else { is_cacheable = 0; } - proto = ce; + proto = cache_key_proto ? cache_key_proto : ce; } /* Delay and record warnings (such as deprecations) thrown during @@ -6516,10 +6568,17 @@ ZEND_API zend_class_entry *zend_synthesize_monomorph( ce->name = canonical; /* Stash the bindings the runtime needs when method bodies reference the - * class-level T directly (e.g. `new T()` inside `class Box`'s body). */ + * class-level T directly (e.g. `new T()` inside `class Box`'s body). + * The bound type is borrowed from extends_payload's NWA — that payload + * gets installed into ce->generic_types below, so it has the same + * lifetime as ce itself, and ce->generic_type_args has the same + * lifetime as ce too. The canonical name is owned by the entry. */ + const zend_type_named_with_args *binding_nwa = + ZEND_TYPE_NAMED_WITH_ARGS(extends_payload); ce->generic_type_args = zend_type_arg_table_alloc(arity); for (uint32_t i = 0; i < arity; i++) { - ce->generic_type_args->names[i] = zend_type_arg_canonical_name(args[i]); + ce->generic_type_args->entries[i].name = zend_type_arg_canonical_name(binding_nwa->args[i]); + ce->generic_type_args->entries[i].type_ref = &binding_nwa->args[i]; } bool base_is_interface = (base->ce_flags & ZEND_ACC_INTERFACE) != 0; @@ -6567,19 +6626,22 @@ ZEND_API zend_class_entry *zend_synthesize_monomorph( /* Monomorph synthesis is an engine-internal child of the base, not a * user-declared subclass. final/readonly restrictions that gate - * user-level extension are temporarily suppressed for the link and then - * re-applied to the monomorph so user code still can't `extends Box` - * if Box itself was final. Abstract is propagated so the monomorph of an - * abstract class stays abstract (and a concrete subclass can implement - * its methods). */ - uint32_t suppressed = base->ce_flags & (ZEND_ACC_FINAL | ZEND_ACC_READONLY_CLASS); + * user-level extension are bypassed via EG(monomorph_synthesis_active) + * for the duration of the link, then propagated to the monomorph so user + * code still can't `extends Box` if Box itself was final. Abstract + * is propagated so the monomorph of an abstract class stays abstract + * (and a concrete subclass can implement its methods). Note: we don't + * mutate base->ce_flags here because base may live in read-only opcache + * SHM (opcache.protect_memory=1). */ + uint32_t inherited = base->ce_flags & (ZEND_ACC_FINAL | ZEND_ACC_READONLY_CLASS); uint32_t propagated = base->ce_flags & (ZEND_ACC_EXPLICIT_ABSTRACT_CLASS); - base->ce_flags &= ~suppressed; ce->ce_flags |= propagated; + bool prev_mono_active = EG(monomorph_synthesis_active); + EG(monomorph_synthesis_active) = true; zend_class_entry *linked = zend_do_link_class(ce, parent_lc, lc_canonical); - base->ce_flags |= suppressed; + EG(monomorph_synthesis_active) = prev_mono_active; if (linked) { - linked->ce_flags |= suppressed; + linked->ce_flags |= inherited; } if (parent_lc) zend_string_release(parent_lc); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 9d79e0159663..9dcafdaa1d1a 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -180,6 +180,22 @@ static void zend_generic_type_table_value_dtor(zval *zv) { efree(type); } +/* Dedicated dtor for turbofish_args entries: each one owns both an inline + * args_box (released like a normal boxed type) and a one-slot cached_table + * for the runtime call-cache, which must be torn down here since the cache + * survives across calls and isn't tied to any single frame. */ +static void zend_turbofish_args_entry_dtor(zval *zv) { + zend_turbofish_args_entry *entry = Z_PTR_P(zv); + if (entry->cached_table) { + /* Drop the persisted flag so the destroy path actually releases names + * and frees the table — the cache, not opcache SHM, owns this. */ + entry->cached_table->persisted = false; + zend_type_arg_table_destroy(entry->cached_table); + } + zend_type_release(entry->args_box, /* persistent */ false); + efree(entry); +} + ZEND_API zend_generic_type_table *zend_generic_type_table_alloc(void) { zend_generic_type_table *table = ecalloc(1, sizeof(zend_generic_type_table)); return table; @@ -235,6 +251,16 @@ static HashTable *zend_generic_type_table_ensure_indexed(HashTable **slot) { return *slot; } +static HashTable *zend_generic_type_table_ensure_turbofish(HashTable **slot) { + if (!*slot) { + HashTable *ht; + ALLOC_HASHTABLE(ht); + zend_hash_init(ht, 4, NULL, zend_turbofish_args_entry_dtor, /* persistent */ false); + *slot = ht; + } + return *slot; +} + static HashTable *zend_generic_type_table_ensure_named(HashTable **slot) { if (!*slot) { HashTable *ht; @@ -288,25 +314,44 @@ ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, } ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type) { - zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->turbofish_args), op_num, zend_type_box(type)); + zend_turbofish_args_entry *entry = emalloc(sizeof(*entry)); + entry->args_box = type; + entry->cached_table = NULL; + entry->cache_key = 0; + zend_hash_index_update_ptr(zend_generic_type_table_ensure_turbofish(&t->turbofish_args), op_num, entry); } +/* Monotonic nonce stamped onto every allocated zend_type_arg_table so PIC + * slots can distinguish "same table" from "same address, reused table." 32 + * bits gives ~4 billion allocations between potential rollovers, which is + * fine for in-request scope. */ +static uint32_t zend_type_arg_table_generation_counter = 1; + ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count) { zend_type_arg_table *table = emalloc(ZEND_TYPE_ARG_TABLE_SIZE(count)); table->count = count; + table->generation = zend_type_arg_table_generation_counter++; + table->persisted = false; for (uint32_t i = 0; i < count; i++) { - table->names[i] = NULL; + table->entries[i].name = NULL; + table->entries[i].type_ref = NULL; + table->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); } return table; } ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table) { - if (!table) { + if (!table || table->persisted) { return; } for (uint32_t i = 0; i < table->count; i++) { - if (table->names[i]) { - zend_string_release(table->names[i]); + if (table->entries[i].name) { + zend_string_release(table->entries[i].name); + } + /* type_ref is borrowed — never released here. owned_type is the + * locally-synthesised fallback (inference) and is. */ + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_type_release(table->entries[i].owned_type, /* persistent */ false); } } efree(table); diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c index d51ad7c0356e..1d1a6d5a3b6c 100644 --- a/Zend/zend_operators.c +++ b/Zend/zend_operators.c @@ -2578,8 +2578,8 @@ static bool zend_mono_subtype_under_variance(const zend_class_entry *base, return false; } for (uint32_t i = 0; i < count; i++) { - zend_string *a_name = a->generic_type_args->names[i]; - zend_string *b_name = b->generic_type_args->names[i]; + zend_string *a_name = a->generic_type_args->entries[i].name; + zend_string *b_name = b->generic_type_args->entries[i].name; if (!a_name || !b_name) { return false; } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 1af0cb8fbcf3..6d7e841bad76 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4860,12 +4860,26 @@ ZEND_VM_HANDLER(107, ZEND_CATCH, CONST|UNUSED, JMP_ADDR, LAST_CATCH|CACHE_SLOT) ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (OP1_TYPE == IS_UNUSED) { - /* catch (T $e) / catch (Box $e): the class resolves per execution - * against the runtime T-table — not cached, since the same opline - * runs in monos with different bindings. zend_fetch_class dispatches - * to the right resolver based on the packed sub-type. */ - catch_ce = zend_fetch_class(NULL, - opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); if (UNEXPECTED(catch_ce == NULL)) { @@ -8162,7 +8176,24 @@ ZEND_VM_C_LABEL(try_instanceof): } } } else if (OP2_TYPE == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { FREE_OP1(); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -8993,7 +9024,8 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; SAVE_OPLINE(); @@ -9010,7 +9042,7 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -9065,6 +9097,68 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) ZEND_VM_NEXT_OPCODE(); } +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + + SAVE_OPLINE(); + + if (OP1_TYPE == IS_UNUSED) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + zend_verify_generic_arg_types(call, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } + } + + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + ZEND_VM_HOT_HANDLER(122, ZEND_DEFINED, CONST, ANY, CACHE_SLOT) { USE_OPLINE diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 1bef2068eebb..4c1bd41b673a 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5191,12 +5191,26 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_CO ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (IS_CONST == IS_UNUSED) { - /* catch (T $e) / catch (Box $e): the class resolves per execution - * against the runtime T-table — not cached, since the same opline - * runs in monos with different bindings. zend_fetch_class dispatches - * to the right resolver based on the packed sub-type. */ - catch_ce = zend_fetch_class(NULL, - opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); if (UNEXPECTED(catch_ce == NULL)) { @@ -19736,7 +19750,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_CONST == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -21390,7 +21421,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_VAR == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -21917,7 +21965,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_UNUSED == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -22067,7 +22132,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; SAVE_OPLINE(); @@ -22084,7 +22150,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -22139,6 +22205,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI ZEND_VM_NEXT_OPCODE(); } +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + + SAVE_OPLINE(); + + if (IS_TMP_VAR == IS_UNUSED) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + zend_verify_generic_arg_types(call, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } + } + + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_COUNT_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -32953,12 +33081,26 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_UN ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (IS_UNUSED == IS_UNUSED) { - /* catch (T $e) / catch (Box $e): the class resolves per execution - * against the runtime T-table — not cached, since the same opline - * runs in monos with different bindings. zend_fetch_class dispatches - * to the right resolver based on the packed sub-type. */ - catch_ce = zend_fetch_class(NULL, - opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); if (UNEXPECTED(catch_ce == NULL)) { @@ -37595,7 +37737,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; SAVE_OPLINE(); @@ -37612,7 +37755,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -37667,6 +37810,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI ZEND_VM_NEXT_OPCODE(); } +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + + SAVE_OPLINE(); + + if (IS_UNUSED == IS_UNUSED) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + zend_verify_generic_arg_types(call, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } + } + + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -44907,7 +45112,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_CONST == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -48728,7 +48950,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_VAR == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -50108,7 +50347,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_UNUSED == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -58247,12 +58503,26 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_CONST_T ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (IS_CONST == IS_UNUSED) { - /* catch (T $e) / catch (Box $e): the class resolves per execution - * against the runtime T-table — not cached, since the same opline - * runs in monos with different bindings. zend_fetch_class dispatches - * to the right resolver based on the packed sub-type. */ - catch_ce = zend_fetch_class(NULL, - opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); if (UNEXPECTED(catch_ce == NULL)) { @@ -72690,7 +72960,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM } } } else if (IS_CONST == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -74344,7 +74631,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM } } } else if (IS_VAR == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -74771,7 +75075,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM } } } else if (IS_UNUSED == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -74921,7 +75242,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; SAVE_OPLINE(); @@ -74938,7 +75260,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -74993,6 +75315,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG ZEND_VM_NEXT_OPCODE(); } +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + + SAVE_OPLINE(); + + if (IS_TMP_VAR == IS_UNUSED) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + zend_verify_generic_arg_types(call, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } + } + + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_COUNT_SPEC_TMP_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -85807,12 +86191,26 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_UNUSED_ ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } if (IS_UNUSED == IS_UNUSED) { - /* catch (T $e) / catch (Box $e): the class resolves per execution - * against the runtime T-table — not cached, since the same opline - * runs in monos with different bindings. zend_fetch_class dispatches - * to the right resolver based on the packed sub-type. */ - catch_ce = zend_fetch_class(NULL, - opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } } else { catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); if (UNEXPECTED(catch_ce == NULL)) { @@ -90449,7 +90847,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - const zend_type *args_box = zend_generic_get_turbofish_args(&EX(func)->op_array, opline->extended_value); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; SAVE_OPLINE(); @@ -90466,7 +90865,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -90521,6 +90920,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG ZEND_VM_NEXT_OPCODE(); } +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); + const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + + SAVE_OPLINE(); + + if (IS_UNUSED == IS_UNUSED) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + zend_verify_generic_arg_types(call, args_box); + } else { + zval *new_obj = EX_VAR(opline->op1.var); + zend_class_entry *ce = Z_OBJCE_P(new_obj); + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + if (ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } + } + } + } + + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -97761,7 +98222,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV } } } else if (IS_CONST == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -101582,7 +102060,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV } } } else if (IS_VAR == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -102860,7 +103355,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV } } } else if (IS_UNUSED == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -110016,6 +110528,11 @@ ZEND_API void execute_ex(zend_execute_data *ex) (void*)&&ZEND_NULL_LABEL, (void*)&&ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_LABEL, (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, (void*)&&ZEND_INIT_FCALL_OFFSET_SPEC_CONST_LABEL, (void*)&&ZEND_RECV_NOTYPE_SPEC_LABEL, (void*)&&ZEND_NULL_LABEL, @@ -113753,6 +114270,11 @@ ZEND_API void execute_ex(zend_execute_data *ex) ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); VM_TRACE_OP_END(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) HYBRID_BREAK(); + HYBRID_CASE(ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED): + VM_TRACE(ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED) + ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED) + HYBRID_BREAK(); HYBRID_CASE(ZEND_COUNT_SPEC_TMP_UNUSED): VM_TRACE(ZEND_COUNT_SPEC_TMP_UNUSED) ZEND_COUNT_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); @@ -114868,6 +115390,11 @@ ZEND_API void execute_ex(zend_execute_data *ex) ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); VM_TRACE_OP_END(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) HYBRID_BREAK(); + HYBRID_CASE(ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED): + VM_TRACE(ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED) + ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED) + HYBRID_BREAK(); HYBRID_CASE(ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED): VM_TRACE(ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED) ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); @@ -118978,6 +119505,11 @@ void zend_vm_init(void) ZEND_NULL_HANDLER, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_HANDLER, ZEND_NULL_HANDLER, + ZEND_NULL_HANDLER, + ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_HANDLER, + ZEND_NULL_HANDLER, + ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_HANDLER, + ZEND_NULL_HANDLER, ZEND_INIT_FCALL_OFFSET_SPEC_CONST_HANDLER, ZEND_RECV_NOTYPE_SPEC_HANDLER, ZEND_NULL_HANDLER, @@ -122465,6 +122997,11 @@ void zend_vm_init(void) ZEND_NULL_TAILCALL_HANDLER, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER, ZEND_NULL_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, ZEND_INIT_FCALL_OFFSET_SPEC_CONST_TAILCALL_HANDLER, ZEND_RECV_NOTYPE_SPEC_TAILCALL_HANDLER, ZEND_NULL_TAILCALL_HANDLER, @@ -123433,7 +123970,7 @@ void zend_vm_init(void) 1255, 1256 | SPEC_RULE_OP1, 1261 | SPEC_RULE_OP1, - 3483, + 3488, 1266 | SPEC_RULE_OP1, 1271 | SPEC_RULE_OP1, 1276 | SPEC_RULE_OP2, @@ -123467,7 +124004,7 @@ void zend_vm_init(void) 1559 | SPEC_RULE_OP1 | SPEC_RULE_OP2, 1584 | SPEC_RULE_OP1, 1589, - 3483, + 3488, 1590 | SPEC_RULE_OP1, 1595 | SPEC_RULE_OP1 | SPEC_RULE_OP2, 1620 | SPEC_RULE_OP1 | SPEC_RULE_OP2, @@ -123601,49 +124138,49 @@ void zend_vm_init(void) 2561, 2562, 2563 | SPEC_RULE_OP1, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, - 3483, + 2568 | SPEC_RULE_OP1, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, }; #if 0 #elif (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) @@ -123836,7 +124373,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2576 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2581 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123844,7 +124381,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2601 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2606 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123852,7 +124389,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2626 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2631 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123863,17 +124400,17 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2651 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2656 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } else if (op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2676 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2681 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2701 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2706 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } break; case ZEND_MUL: @@ -123884,17 +124421,17 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2726 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2731 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2751 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2756 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2776 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2781 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_IDENTICAL: @@ -123905,16 +124442,16 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2801 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2806 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2876 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2881 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0)) { - spec = 3101 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3106 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { - spec = 3107 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 3112 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_NOT_IDENTICAL: @@ -123925,16 +124462,16 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2951 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2956 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3026 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3031 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0)) { - spec = 3104 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3109 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { - spec = 3112 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 3117 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_EQUAL: @@ -123945,12 +124482,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2801 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2806 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2876 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2881 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_NOT_EQUAL: @@ -123961,12 +124498,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2951 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2956 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3026 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3031 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_SMALLER: @@ -123974,12 +124511,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3117 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3122 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3192 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3197 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } break; case ZEND_IS_SMALLER_OR_EQUAL: @@ -123987,79 +124524,79 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3267 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3272 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3342 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3347 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } break; case ZEND_QM_ASSIGN: if (op1_info == MAY_BE_LONG) { - spec = 3429 | SPEC_RULE_OP1; - } else if (op1_info == MAY_BE_DOUBLE) { spec = 3434 | SPEC_RULE_OP1; - } else if ((op->op1_type == IS_CONST) ? !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1)) : (!(op1_info & ((MAY_BE_ANY|MAY_BE_UNDEF)-(MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE))))) { + } else if (op1_info == MAY_BE_DOUBLE) { spec = 3439 | SPEC_RULE_OP1; + } else if ((op->op1_type == IS_CONST) ? !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1)) : (!(op1_info & ((MAY_BE_ANY|MAY_BE_UNDEF)-(MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE))))) { + spec = 3444 | SPEC_RULE_OP1; } break; case ZEND_PRE_INC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3417 | SPEC_RULE_RETVAL; + spec = 3422 | SPEC_RULE_RETVAL; } else if (op1_info == MAY_BE_LONG) { - spec = 3419 | SPEC_RULE_RETVAL; + spec = 3424 | SPEC_RULE_RETVAL; } break; case ZEND_PRE_DEC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3421 | SPEC_RULE_RETVAL; + spec = 3426 | SPEC_RULE_RETVAL; } else if (op1_info == MAY_BE_LONG) { - spec = 3423 | SPEC_RULE_RETVAL; + spec = 3428 | SPEC_RULE_RETVAL; } break; case ZEND_POST_INC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3425; + spec = 3430; } else if (op1_info == MAY_BE_LONG) { - spec = 3426; + spec = 3431; } break; case ZEND_POST_DEC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3427; + spec = 3432; } else if (op1_info == MAY_BE_LONG) { - spec = 3428; + spec = 3433; } break; case ZEND_JMP: if (OP_JMP_ADDR(op, op->op1) > op) { - spec = 2575; + spec = 2580; } break; case ZEND_INIT_FCALL: if (Z_EXTRA_P(RT_CONSTANT(op, op->op2)) != 0) { - spec = 2568; + spec = 2573; } break; case ZEND_RECV: if (op->op2.num == MAY_BE_ANY) { - spec = 2569; + spec = 2574; } break; case ZEND_SEND_VAL: if (op->op1_type == IS_CONST && op->op2_type == IS_UNUSED && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1))) { - spec = 3479; + spec = 3484; } break; case ZEND_SEND_VAR_EX: if (op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0) { - spec = 3474 | SPEC_RULE_OP1; + spec = 3479 | SPEC_RULE_OP1; } break; case ZEND_FE_FETCH_R: if (op->op2_type == IS_CV && (op1_info & (MAY_BE_ANY|MAY_BE_REF)) == MAY_BE_ARRAY) { - spec = 3481 | SPEC_RULE_RETVAL; + spec = 3486 | SPEC_RULE_RETVAL; } break; case ZEND_FETCH_DIM_R: @@ -124067,22 +124604,22 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3444 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 3449 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } break; case ZEND_SEND_VAL_EX: if (op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && op->op1_type == IS_CONST && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1))) { - spec = 3480; + spec = 3485; } break; case ZEND_SEND_VAR: if (op->op2_type == IS_UNUSED && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0) { - spec = 3469 | SPEC_RULE_OP1; + spec = 3474 | SPEC_RULE_OP1; } break; case ZEND_COUNT: if ((op1_info & (MAY_BE_ANY|MAY_BE_UNDEF|MAY_BE_REF)) == MAY_BE_ARRAY) { - spec = 2570 | SPEC_RULE_OP1; + spec = 2575 | SPEC_RULE_OP1; } break; case ZEND_BW_OR: diff --git a/Zend/zend_vm_handlers.h b/Zend/zend_vm_handlers.h index ede9bb61f95e..b1ff354531a5 100644 --- a/Zend/zend_vm_handlers.h +++ b/Zend/zend_vm_handlers.h @@ -1090,507 +1090,509 @@ _(2562, ZEND_TYPE_ASSERT_SPEC_CONST) \ _(2564, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) \ _(2566, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) \ - _(2568, ZEND_INIT_FCALL_OFFSET_SPEC_CONST) \ - _(2569, ZEND_RECV_NOTYPE_SPEC) \ - _(2571, ZEND_COUNT_ARRAY_SPEC_TMP_UNUSED) \ - _(2574, ZEND_COUNT_ARRAY_SPEC_CV_UNUSED) \ - _(2575, ZEND_JMP_FORWARD_SPEC) \ - _(2581, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2582, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2583, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2585, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2569, ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED) \ + _(2571, ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED) \ + _(2573, ZEND_INIT_FCALL_OFFSET_SPEC_CONST) \ + _(2574, ZEND_RECV_NOTYPE_SPEC) \ + _(2576, ZEND_COUNT_ARRAY_SPEC_TMP_UNUSED) \ + _(2579, ZEND_COUNT_ARRAY_SPEC_CV_UNUSED) \ + _(2580, ZEND_JMP_FORWARD_SPEC) \ _(2586, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ _(2587, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2588, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2590, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2596, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2597, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2598, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2600, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2606, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ - _(2607, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2608, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2610, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2591, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2592, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2593, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2595, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2601, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2602, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2603, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2605, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2611, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ _(2612, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2613, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2615, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2621, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ - _(2622, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2623, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2625, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2631, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2632, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2633, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2635, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2616, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2617, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2618, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2620, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2626, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2627, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2628, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2630, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2636, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ _(2637, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2638, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2640, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2646, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2647, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2648, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2650, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2652, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2653, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2655, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2656, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2657, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2658, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2660, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2641, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2642, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2643, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2645, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2651, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2652, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2653, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2655, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2657, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2658, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2660, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ _(2661, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ _(2662, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2663, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2665, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2671, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2672, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2673, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2675, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2677, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2678, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2680, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2681, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ - _(2682, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2683, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2685, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2666, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2667, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2668, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2670, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2676, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2677, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2678, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2680, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2682, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2683, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2685, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ _(2686, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ _(2687, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2688, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2690, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2696, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ - _(2697, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2698, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2700, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2702, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2703, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2705, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2706, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2707, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2708, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2710, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2691, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2692, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2693, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2695, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2701, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2702, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2703, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2705, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2707, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2708, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2710, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ _(2711, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ _(2712, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2713, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2715, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2721, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2722, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2723, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2725, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2731, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2732, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2733, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2735, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2716, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2717, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2718, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2720, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2726, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2727, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2728, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2730, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2736, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ _(2737, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2738, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2740, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2746, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2747, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2748, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2750, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2756, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ - _(2757, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2758, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2760, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2741, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2742, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2743, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2745, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2751, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2752, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2753, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2755, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2761, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ _(2762, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2763, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2765, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2771, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ - _(2772, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2773, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2775, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2781, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2782, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2783, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2785, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2766, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2767, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2768, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2770, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2776, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2777, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2778, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2780, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2786, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ _(2787, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2788, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2790, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2796, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2797, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2798, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2800, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2816, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2817, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2818, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2819, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2820, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2821, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2822, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2823, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2824, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2828, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2829, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2830, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2831, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2832, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2833, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2834, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2835, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2836, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2837, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2838, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2839, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2843, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2844, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2845, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2861, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2862, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2863, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2864, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2865, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2866, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2867, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2868, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2869, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2873, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2874, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2875, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2891, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2892, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2893, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2894, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2895, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2896, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2897, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2898, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2899, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2903, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2904, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2905, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2906, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2907, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2908, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2909, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2910, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2911, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2912, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2913, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2914, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2918, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2919, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2920, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2936, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2937, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2938, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2939, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2940, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2941, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2942, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2943, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2944, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2948, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2949, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2950, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2966, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2967, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2968, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2969, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2970, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2971, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2972, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2973, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2974, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2978, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2979, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2980, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2981, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2982, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2983, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2984, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2985, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2986, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2987, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2988, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2989, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2993, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2994, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2995, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3011, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3012, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3013, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3014, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3015, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3016, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3017, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3018, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3019, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3023, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3024, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3025, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3041, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3042, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3043, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3044, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3045, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3046, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3047, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3048, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3049, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3053, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3054, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3055, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3056, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3057, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3058, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3059, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3060, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3061, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3062, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3063, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3064, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3068, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3069, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3070, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3086, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3087, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3088, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3089, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3090, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3091, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3092, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3093, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3094, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3098, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3099, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3100, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3101, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ - _(3102, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3103, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3104, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ - _(3105, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3106, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3107, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ - _(3111, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CV) \ - _(3112, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ - _(3116, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CV) \ - _(3120, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3121, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3122, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3123, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3124, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3125, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3129, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3130, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3131, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3132, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3133, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3134, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3135, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3136, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3137, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3138, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3139, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3140, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3144, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3145, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3146, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3147, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3148, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3149, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3150, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3151, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3152, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3153, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3154, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3155, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3159, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3160, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3161, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3177, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3178, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3179, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3180, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3181, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3182, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3183, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3184, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3185, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3189, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3190, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3191, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3195, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3196, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3197, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3198, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3199, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3200, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3204, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3205, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3206, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3207, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3208, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3209, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3210, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3211, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3212, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3213, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3214, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3215, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3219, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3220, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3221, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3222, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3223, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3224, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3225, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3226, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3227, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3228, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3229, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3230, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3234, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3235, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3236, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3252, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3253, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3254, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3255, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3256, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3257, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3258, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3259, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3260, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3264, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3265, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3266, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3270, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3271, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3272, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3273, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3274, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3275, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3279, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3280, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3281, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3282, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3283, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3284, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3285, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3286, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3287, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3288, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3289, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3290, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3294, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3295, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3296, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3297, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3298, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3299, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3300, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3301, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3302, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3303, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3304, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3305, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3309, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3310, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3311, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3327, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3328, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3329, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3330, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3331, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3332, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3333, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3334, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3335, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3339, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3340, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3341, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3345, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3346, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3347, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3348, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3349, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3350, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3354, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3355, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3356, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3357, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3358, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3359, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3360, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3361, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3362, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3363, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3364, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3365, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3369, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3370, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3371, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3372, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3373, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3374, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3375, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3376, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3377, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3378, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3379, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3380, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3384, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3385, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3386, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3402, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3403, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3404, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3405, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3406, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3407, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3408, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3409, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3410, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3414, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3415, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3416, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3417, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ - _(3418, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ - _(3419, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_UNUSED) \ - _(3420, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_USED) \ - _(3421, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ - _(3422, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ - _(3423, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_UNUSED) \ - _(3424, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_USED) \ - _(3425, ZEND_POST_INC_LONG_NO_OVERFLOW_SPEC_CV) \ - _(3426, ZEND_POST_INC_LONG_SPEC_CV) \ - _(3427, ZEND_POST_DEC_LONG_NO_OVERFLOW_SPEC_CV) \ - _(3428, ZEND_POST_DEC_LONG_SPEC_CV) \ - _(3429, ZEND_QM_ASSIGN_LONG_SPEC_CONST) \ - _(3430, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3431, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3433, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3434, ZEND_QM_ASSIGN_DOUBLE_SPEC_CONST) \ - _(3435, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3436, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3438, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3439, ZEND_QM_ASSIGN_NOREF_SPEC_CONST) \ - _(3440, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3441, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3443, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3445, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3446, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3448, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3449, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ - _(3450, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3451, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3453, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(2791, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2792, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2793, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2795, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2801, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2802, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2803, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2805, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2821, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2822, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2823, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2824, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2825, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2826, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2827, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2828, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2829, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2833, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2834, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2835, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2836, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2837, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2838, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2839, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2840, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2841, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2842, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2843, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2844, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2848, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2849, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2850, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2866, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2867, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2868, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2869, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2870, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2871, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2872, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2873, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2874, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2878, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2879, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2880, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2896, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2897, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2898, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2899, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2900, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2901, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2902, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2903, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2904, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2908, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2909, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2910, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2911, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2912, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2913, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2914, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2915, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2916, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2917, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2918, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2919, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2923, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2924, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2925, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2941, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2942, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2943, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2944, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2945, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2946, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2947, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2948, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2949, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2953, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2954, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2955, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2971, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2972, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2973, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2974, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2975, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2976, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2977, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2978, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2979, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2983, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2984, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2985, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2986, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2987, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2988, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2989, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2990, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2991, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2992, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2993, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2994, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2998, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2999, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3000, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3016, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3017, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3018, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3019, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3020, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3021, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3022, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3023, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3024, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3028, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3029, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3030, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3046, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3047, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3048, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3049, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3050, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3051, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3052, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3053, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3054, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3058, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3059, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3060, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3061, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3062, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3063, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3064, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3065, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3066, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3067, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3068, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3069, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3073, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3074, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3075, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3091, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3092, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3093, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3094, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3095, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3096, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3097, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3098, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3099, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3103, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3104, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3105, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3106, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ + _(3107, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3108, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3109, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ + _(3110, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3111, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3112, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ + _(3116, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CV) \ + _(3117, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ + _(3121, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CV) \ + _(3125, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3126, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3127, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3128, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3129, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3130, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3134, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3135, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3136, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3137, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3138, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3139, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3140, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3141, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3142, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3143, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3144, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3145, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3149, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3150, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3151, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3152, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3153, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3154, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3155, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3156, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3157, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3158, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3159, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3160, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3164, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3165, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3166, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3182, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3183, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3184, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3185, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3186, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3187, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3188, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3189, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3190, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3194, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3195, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3196, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3200, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3201, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3202, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3203, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3204, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3205, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3209, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3210, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3211, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3212, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3213, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3214, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3215, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3216, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3217, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3218, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3219, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3220, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3224, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3225, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3226, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3227, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3228, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3229, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3230, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3231, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3232, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3233, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3234, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3235, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3239, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3240, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3241, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3257, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3258, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3259, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3260, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3261, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3262, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3263, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3264, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3265, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3269, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3270, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3271, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3275, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3276, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3277, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3278, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3279, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3280, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3284, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3285, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3286, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3287, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3288, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3289, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3290, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3291, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3292, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3293, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3294, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3295, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3299, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3300, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3301, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3302, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3303, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3304, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3305, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3306, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3307, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3308, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3309, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3310, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3314, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3315, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3316, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3332, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3333, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3334, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3335, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3336, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3337, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3338, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3339, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3340, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3344, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3345, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3346, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3350, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3351, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3352, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3353, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3354, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3355, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3359, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3360, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3361, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3362, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3363, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3364, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3365, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3366, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3367, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3368, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3369, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3370, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3374, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3375, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3376, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3377, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3378, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3379, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3380, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3381, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3382, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3383, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3384, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3385, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3389, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3390, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3391, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3407, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3408, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3409, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3410, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3411, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3412, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3413, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3414, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3415, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3419, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3420, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3421, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3422, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ + _(3423, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ + _(3424, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_UNUSED) \ + _(3425, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_USED) \ + _(3426, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ + _(3427, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ + _(3428, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_UNUSED) \ + _(3429, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_USED) \ + _(3430, ZEND_POST_INC_LONG_NO_OVERFLOW_SPEC_CV) \ + _(3431, ZEND_POST_INC_LONG_SPEC_CV) \ + _(3432, ZEND_POST_DEC_LONG_NO_OVERFLOW_SPEC_CV) \ + _(3433, ZEND_POST_DEC_LONG_SPEC_CV) \ + _(3434, ZEND_QM_ASSIGN_LONG_SPEC_CONST) \ + _(3435, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3436, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3438, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3439, ZEND_QM_ASSIGN_DOUBLE_SPEC_CONST) \ + _(3440, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3441, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3443, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3444, ZEND_QM_ASSIGN_NOREF_SPEC_CONST) \ + _(3445, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3446, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3448, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3450, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3451, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3453, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ _(3454, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ _(3455, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ _(3456, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ _(3458, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3464, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_CONST) \ - _(3465, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3466, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3468, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3471, ZEND_SEND_VAR_SIMPLE_SPEC_VAR) \ - _(3473, ZEND_SEND_VAR_SIMPLE_SPEC_CV) \ - _(3476, ZEND_SEND_VAR_EX_SIMPLE_SPEC_VAR_UNUSED) \ - _(3478, ZEND_SEND_VAR_EX_SIMPLE_SPEC_CV_UNUSED) \ - _(3479, ZEND_SEND_VAL_SIMPLE_SPEC_CONST) \ - _(3480, ZEND_SEND_VAL_EX_SIMPLE_SPEC_CONST) \ - _(3481, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_UNUSED) \ - _(3482, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) \ - _(3482+1, ZEND_NULL) + _(3459, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ + _(3460, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3461, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3463, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3469, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_CONST) \ + _(3470, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3471, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3473, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3476, ZEND_SEND_VAR_SIMPLE_SPEC_VAR) \ + _(3478, ZEND_SEND_VAR_SIMPLE_SPEC_CV) \ + _(3481, ZEND_SEND_VAR_EX_SIMPLE_SPEC_VAR_UNUSED) \ + _(3483, ZEND_SEND_VAR_EX_SIMPLE_SPEC_CV_UNUSED) \ + _(3484, ZEND_SEND_VAL_SIMPLE_SPEC_CONST) \ + _(3485, ZEND_SEND_VAL_EX_SIMPLE_SPEC_CONST) \ + _(3486, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_UNUSED) \ + _(3487, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) \ + _(3487+1, ZEND_NULL) diff --git a/Zend/zend_vm_opcodes.c b/Zend/zend_vm_opcodes.c index accebcfae814..84e541d4de06 100644 --- a/Zend/zend_vm_opcodes.c +++ b/Zend/zend_vm_opcodes.c @@ -21,7 +21,7 @@ #include #include -static const char *zend_vm_opcodes_names[213] = { +static const char *zend_vm_opcodes_names[214] = { "ZEND_NOP", "ZEND_ADD", "ZEND_SUB", @@ -235,9 +235,10 @@ static const char *zend_vm_opcodes_names[213] = { "ZEND_DECLARE_ATTRIBUTED_CONST", "ZEND_TYPE_ASSERT", "ZEND_VERIFY_GENERIC_ARGUMENTS", + "ZEND_INSTALL_GENERIC_ARGS", }; -static uint32_t zend_vm_opcodes_flags[213] = { +static uint32_t zend_vm_opcodes_flags[214] = { 0x00000000, 0x00000b0b, 0x00000b0b, @@ -451,6 +452,7 @@ static uint32_t zend_vm_opcodes_flags[213] = { 0x00000303, 0x01000003, 0x00000101, + 0x00000101, }; ZEND_API const char* ZEND_FASTCALL zend_get_opcode_name(uint8_t opcode) { diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h index b60aad72e1ba..1c4b487196d2 100644 --- a/Zend/zend_vm_opcodes.h +++ b/Zend/zend_vm_opcodes.h @@ -332,7 +332,8 @@ END_EXTERN_C() #define ZEND_DECLARE_ATTRIBUTED_CONST 210 #define ZEND_TYPE_ASSERT 211 #define ZEND_VERIFY_GENERIC_ARGUMENTS 212 +#define ZEND_INSTALL_GENERIC_ARGS 213 -#define ZEND_VM_LAST_OPCODE 212 +#define ZEND_VM_LAST_OPCODE 213 #endif diff --git a/bench/bench_reification.php b/bench/bench_reification.php index 9d25f66d8e49..5b1b600093d6 100644 --- a/bench/bench_reification.php +++ b/bench/bench_reification.php @@ -236,6 +236,90 @@ function plain_arg_check(int $n): void { } } +// -------------------------------------------------------------------------- +// Scenario 9: forwarded T-refs — `id::($x)` inside a generic wrapper. +// Exercises (a) per-call zend_type_arg_table build via the borrowed-pointer +// chain, (b) zend_check_user_type_slow on the substituted type, and (c) the +// VERIFY path that INSTALL can't elide (T-refs in turbofish). +// -------------------------------------------------------------------------- +function idGen(T $x): T { return $x; } +function nestedGen(U $x): U { return idGen::($x); } +interface Fooable {} +interface Barable {} +class FooBar implements Fooable, Barable {} + +function forwarded_scalar(int $n): void { + for ($i = 0; $i < $n; $i++) { + nestedGen::($i); + } +} +function direct_scalar(int $n): void { + for ($i = 0; $i < $n; $i++) { + idGen::($i); + } +} +function forwarded_class(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + nestedGen::($f); + } +} +function direct_class(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + idGen::($f); + } +} +function forwarded_union(int $n): void { + for ($i = 0; $i < $n; $i++) { + nestedGen::($i); + } +} +function forwarded_intersection(int $n): void { + $fb = new FooBar(); + for ($i = 0; $i < $n; $i++) { + nestedGen::($fb); + } +} + +// -------------------------------------------------------------------------- +// Scenario 10: generic instanceof/catch in a hot inner loop — exercises the +// polymorphic inline cache on the IS_UNUSED FETCH_CLASS path. The first +// iteration misses; subsequent iterations within the same frame hit on a +// pointer compare against (type_args generation, called_scope). +// -------------------------------------------------------------------------- +class Container {} + +function instanceof_t_hot_loop(array $items): int { + $count = 0; + foreach ($items as $item) { + if ($item instanceof Container) { + $count++; + } + } + return $count; +} +function instanceof_concrete_hot_loop(array $items): int { + $count = 0; + foreach ($items as $item) { + if ($item instanceof Container) { + $count++; + } + } + return $count; +} + +function bench_instanceof_t(int $n): void { + $items = [new Container::(), new Container::(), new Foo()]; + /* outer N drives the inner-loop count via a fixed batch — keeps the + * PIC slot warm across iterations of the body. */ + instanceof_t_hot_loop::(array_fill(0, $n, $items[0])); +} +function bench_instanceof_concrete(int $n): void { + $items = [new Container::(), new Container::(), new Foo()]; + instanceof_concrete_hot_loop(array_fill(0, $n, $items[0])); +} + // -------------------------------------------------------------------------- // Driver // -------------------------------------------------------------------------- @@ -296,5 +380,34 @@ function plain_arg_check(int $n): void { $plain_arg_ns = bench("takesPlain(\$f)", 'plain_arg_check'); compare("delta: reified arg check vs plain typed arg", $reified_arg_ns, "reif", $plain_arg_ns, "plain"); -echo "\nNote: opcache is disabled (php -n). Same build, same interpreter\n"; +section("9. Forwarded T-refs (nested generic calls)"); +$fwd_scalar_ns = bench("nestedGen::(\$i) — forwarded scalar", 'forwarded_scalar'); +$dir_scalar_ns = bench("idGen::(\$i) — direct scalar", 'direct_scalar'); +compare("delta: forwarded scalar vs direct scalar", $fwd_scalar_ns, "fwd", $dir_scalar_ns, "dir"); +$fwd_class_ns = bench("nestedGen::(\$f) — forwarded class", 'forwarded_class'); +$dir_class_ns = bench("idGen::(\$f) — direct class", 'direct_class'); +compare("delta: forwarded class vs direct class", $fwd_class_ns, "fwd", $dir_class_ns, "dir"); +$fwd_union_ns = bench("nestedGen:: — forwarded union", 'forwarded_union', ITERS_LIGHT); +$fwd_inter_ns = bench("nestedGen:: — forwarded ix", 'forwarded_intersection', ITERS_LIGHT); +compare("delta: forwarded union vs forwarded scalar", $fwd_union_ns, "uni", $fwd_scalar_ns, "scl"); + +section("10. Generic instanceof in hot inner loop (PIC)"); +$inst_t_ns = bench("instanceof Container (PIC warm)", 'bench_instanceof_t'); +$inst_concrete_ns = bench("instanceof Container (cache slot)", 'bench_instanceof_concrete'); +compare("delta: instanceof T vs instanceof bare", $inst_t_ns, "T", $inst_concrete_ns, "bare"); + +$opcache_loaded = extension_loaded('Zend OPcache'); +$opcache_on = $opcache_loaded + && function_exists('opcache_get_status') + && (bool) (opcache_get_status(false)['opcache_enabled'] ?? false); +$jit_on = false; +if ($opcache_on) { + $status = opcache_get_status(false); + /* 'enabled' = JIT is built into this opcache; 'on' = JIT is active at runtime */ + $jit_on = (bool) ($status['jit']['on'] ?? false); +} +$opcache_label = $opcache_on + ? ('enabled' . ($jit_on ? ' + JIT' : ', JIT off')) + : ($opcache_loaded ? 'loaded but disabled (opcache.enable_cli=0)' : 'not loaded'); +echo "\nNote: opcache is $opcache_label. Same build, same interpreter\n"; echo " path across all scenarios — deltas between rows are the signal.\n"; diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index fbbfab6b243c..8db1b17943c2 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -2933,6 +2933,18 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op } } } + /* Same teardown gap as in the tracing path: the inline CV loop + * skips zend_free_compiled_variables, so a generic-call frame's + * EX(type_args) needs an explicit destroy here. */ + if (!left_frame) { + if (!zend_jit_leave_frame(&ctx)) { + goto jit_failure; + } + left_frame = true; + } + if (!zend_jit_free_type_args(&ctx)) { + goto jit_failure; + } if (!zend_jit_leave_func(&ctx, op_array, NULL, MAY_BE_ANY, left_frame, NULL, NULL, (ssa->cfg.flags & ZEND_FUNC_INDIRECT_VAR_ACCESS) != 0, 1)) { goto jit_failure; diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index cf43d3ad840f..40af0165887d 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -8744,6 +8744,12 @@ static int zend_jit_push_call_frame(zend_jit_ctx *jit, const zend_op *opline, co // JIT: ZEND_CALL_NUM_ARGS(call) = num_args; ir_STORE(jit_CALL(rx, This.u2.num_args), ir_CONST_U32(opline->extended_value)); + // JIT: call->type_args = NULL; + // Mirrors zend_vm_init_call_frame; without this the field is stack + // garbage and zend_vm_stack_free_call_frame_ex dereferences it on + // frame teardown (segfault on the exception / undef-prop path). + ir_STORE(jit_CALL(rx, type_args), IR_NULL); + return 1; } @@ -11039,6 +11045,23 @@ static int zend_jit_free_cvs(zend_jit_ctx *jit) return 1; } +/* The inline CV-free fast path that callers use when last_var <= 100 frees + * each CV individually and bypasses zend_free_compiled_variables, which is + * where the interpreter teardown destroys EX(type_args). Without this helper + * the table allocated by VERIFY_GENERIC_ARGUMENTS / INSTALL_GENERIC_ARGS + * leaks on every JIT'd generic call return — and for inference calls each + * leaked table also carries refcounted owned_type fragments, so the rate is + * fast enough to OOM tight loops in seconds. */ +static int zend_jit_free_type_args(zend_jit_ctx *jit) +{ + ir_ref ta = ir_LOAD_A(jit_EX(type_args)); + ir_ref if_ta = ir_IF(ta); + ir_IF_TRUE_cold(if_ta); + ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_type_arg_table_destroy), ta); + ir_MERGE_WITH_EMPTY_FALSE(if_ta); + return 1; +} + static int zend_jit_free_cv(zend_jit_ctx *jit, uint32_t info, uint32_t var) { if (info & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_REF)) { diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 024a5d0e194d..d5d1f5b4238b 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -5646,6 +5646,20 @@ static zend_vm_opcode_handler_t zend_jit_trace(zend_jit_trace_rec *trace_buffer, } } } + /* The inline CV-free loop above bypasses the + * zend_free_compiled_variables path that destroys + * EX(type_args), so a generic-call frame with a + * non-cached (frame-owned) type-arg table would + * leak it on every return. Mirror the slow path. */ + if (!left_frame) { + if (!zend_jit_leave_frame(&ctx)) { + goto jit_failure; + } + left_frame = 1; + } + if (!zend_jit_free_type_args(&ctx)) { + goto jit_failure; + } if (!zend_jit_leave_func(&ctx, op_array, opline, op1_info, left_frame, p + 1, &zend_jit_traces[ZEND_JIT_TRACE_NUM], (op_array_ssa->cfg.flags & ZEND_FUNC_INDIRECT_VAR_ACCESS) != 0, may_throw)) { diff --git a/ext/opcache/tests/opt/jmp_001.phpt b/ext/opcache/tests/opt/jmp_001.phpt index d5491dfe1ab4..c061d336c4b0 100644 --- a/ext/opcache/tests/opt/jmp_001.phpt +++ b/ext/opcache/tests/opt/jmp_001.phpt @@ -24,10 +24,11 @@ $_main: 0000 RETURN int(1) test: - ; (lines=4, args=0, vars=1, tmps=1) + ; (lines=5, args=0, vars=1, tmps=1) ; (after optimizer) ; %s:2-6 0000 INIT_FCALL_BY_NAME 0 string("test2") -0001 T1 = DO_FCALL_BY_NAME -0002 CV0($var) = QM_ASSIGN T1 -0003 RETURN CV0($var) +0001 VERIFY_GENERIC_ARGUMENTS +0002 T1 = DO_FCALL_BY_NAME +0003 CV0($var) = QM_ASSIGN T1 +0004 RETURN CV0($var) diff --git a/ext/opcache/tests/pipe_optimizations.phpt b/ext/opcache/tests/pipe_optimizations.phpt index 5115eaff59a2..018fbe79f42a 100644 --- a/ext/opcache/tests/pipe_optimizations.phpt +++ b/ext/opcache/tests/pipe_optimizations.phpt @@ -45,8 +45,8 @@ $_main: 0004 SEND_VAL int(5) 1 0005 T2 = DO_UCALL 0006 INIT_METHOD_CALL 1 CV0($o) string("foo") -0007 SEND_VAL_EX T2 1 -0008 T2 = DO_FCALL +0007 SEND_VAL T2 1 +0008 T2 = DO_UCALL 0009 INIT_STATIC_METHOD_CALL 1 string("Other") string("bar") 0010 SEND_VAL T2 1 0011 T2 = DO_UCALL @@ -57,7 +57,6 @@ $_main: 0016 RETURN int(1) LIVE RANGES: 2: 0001 - 0002 (new) - 2: 0009 - 0010 (tmp/var) _test1: ; (lines=4, args=1, vars=1, tmps=%d) diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index 228caf548bfc..09e0d6347960 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -534,6 +534,21 @@ static void zend_file_cache_serialize_generic_type_entry( zend_file_cache_serialize_type(boxed, script, info, buf); } +/* Serialise a turbofish_args entry — same shape as a boxed type from the + * file's perspective but the buffer slot is the larger entry struct. Runtime + * cache fields (cached_table, cache_key) are not persisted; the unserialize + * side resets them to a cold state. */ +static void zend_file_cache_serialize_turbofish_args_entry( + zval *zv, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + SERIALIZE_PTR(Z_PTR_P(zv)); + zend_turbofish_args_entry *entry = Z_PTR_P(zv); + UNSERIALIZE_PTR(entry); + zend_file_cache_serialize_type(&entry->args_box, script, info, buf); + entry->cached_table = NULL; + entry->cache_key = 0; +} + static void zend_file_cache_serialize_generic_type_table_ht( HashTable **ht_ptr, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) { @@ -544,6 +559,16 @@ static void zend_file_cache_serialize_generic_type_table_ht( zend_file_cache_serialize_hash(ht, script, info, buf, zend_file_cache_serialize_generic_type_entry); } +static void zend_file_cache_serialize_turbofish_args_ht( + HashTable **ht_ptr, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + HashTable *ht; + SERIALIZE_PTR(*ht_ptr); + ht = *ht_ptr; + UNSERIALIZE_PTR(ht); + zend_file_cache_serialize_hash(ht, script, info, buf, zend_file_cache_serialize_turbofish_args_entry); +} + static void zend_file_cache_serialize_generic_parameter_list( zend_generic_parameter_list **list_ptr, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) @@ -597,7 +622,28 @@ static void zend_file_cache_serialize_generic_type_table( zend_file_cache_serialize_generic_type_table_ht(&table->trait_uses, script, info, buf); } if (table->turbofish_args) { - zend_file_cache_serialize_generic_type_table_ht(&table->turbofish_args, script, info, buf); + zend_file_cache_serialize_turbofish_args_ht(&table->turbofish_args, script, info, buf); + } +} + +/* Serialise a monomorph's generic_type_args. The owned strings (names) and + * owned_type payloads are inline within the table — type_ref is a borrowed + * pointer that we rebuild from ce->generic_types on the unserialise side, so + * we just NULL it out here. */ +static void zend_file_cache_serialize_type_arg_table( + zend_type_arg_table **table_ptr, zend_persistent_script *script, + zend_file_cache_metainfo *info, void *buf) +{ + zend_type_arg_table *table; + SERIALIZE_PTR(*table_ptr); + table = *table_ptr; + UNSERIALIZE_PTR(table); + for (uint32_t i = 0; i < table->count; i++) { + SERIALIZE_STR(table->entries[i].name); + table->entries[i].type_ref = NULL; + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_file_cache_serialize_type(&table->entries[i].owned_type, script, info, buf); + } } } @@ -958,6 +1004,10 @@ static void zend_file_cache_serialize_class(zval *zv, zend_file_cache_serialize_generic_type_table(&ce->generic_types, script, info, buf); } + if (ce->generic_type_args) { + zend_file_cache_serialize_type_arg_table(&ce->generic_type_args, script, info, buf); + } + zend_file_cache_serialize_hash(&ce->properties_info, script, info, buf, zend_file_cache_serialize_prop_info); if (ce->properties_info_table) { @@ -1572,6 +1622,17 @@ static void zend_file_cache_unserialize_generic_type_entry( (zend_type *) Z_PTR_P(zv), zend_file_cache_generic_unserialize_scope, script, buf); } +static void zend_file_cache_unserialize_turbofish_args_entry( + zval *zv, zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(Z_PTR_P(zv)); + zend_turbofish_args_entry *entry = Z_PTR_P(zv); + zend_file_cache_unserialize_type( + &entry->args_box, zend_file_cache_generic_unserialize_scope, script, buf); + entry->cached_table = NULL; + entry->cache_key = 0; +} + static void zend_file_cache_unserialize_generic_type_table_ht( HashTable **ht_ptr, zend_class_entry *scope, zend_persistent_script *script, void *buf) { @@ -1583,6 +1644,17 @@ static void zend_file_cache_unserialize_generic_type_table_ht( zend_file_cache_generic_unserialize_scope = prev; } +static void zend_file_cache_unserialize_turbofish_args_ht( + HashTable **ht_ptr, zend_class_entry *scope, zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*ht_ptr); + HashTable *ht = *ht_ptr; + zend_class_entry *prev = zend_file_cache_generic_unserialize_scope; + zend_file_cache_generic_unserialize_scope = scope; + zend_file_cache_unserialize_hash(ht, script, buf, zend_file_cache_unserialize_turbofish_args_entry, NULL); + zend_file_cache_generic_unserialize_scope = prev; +} + static void zend_file_cache_unserialize_generic_parameter_list( zend_generic_parameter_list **list_ptr, zend_class_entry *scope, zend_persistent_script *script, void *buf) @@ -1635,7 +1707,48 @@ static void zend_file_cache_unserialize_generic_type_table( } if (table->turbofish_args) { - zend_file_cache_unserialize_generic_type_table_ht(&table->turbofish_args, scope, script, buf); + zend_file_cache_unserialize_turbofish_args_ht(&table->turbofish_args, scope, script, buf); + } +} + +/* Mirror of zend_file_cache_serialize_type_arg_table. Rebuilds `type_ref` + * to point into the now-unserialized ce->generic_types side table — same + * logic as zend_persist_mono_binding_nwa in zend_persist.c. */ +static void zend_file_cache_unserialize_type_arg_table( + zend_type_arg_table **table_ptr, zend_class_entry *scope, + zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*table_ptr); + zend_type_arg_table *table = *table_ptr; + const zend_type_named_with_args *new_nwa = NULL; + if (scope && scope->generic_types) { + if (scope->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*scope->generic_types->extends)) { + new_nwa = ZEND_TYPE_NAMED_WITH_ARGS(*scope->generic_types->extends); + } else { + HashTable *tables[] = { scope->generic_types->implements, + scope->generic_types->trait_uses }; + for (size_t k = 0; k < sizeof(tables) / sizeof(tables[0]); k++) { + if (!tables[k] || zend_hash_num_elements(tables[k]) == 0) continue; + zval *zv; + ZEND_HASH_FOREACH_VAL(tables[k], zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (boxed && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + new_nwa = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + } + break; + } ZEND_HASH_FOREACH_END(); + if (new_nwa) break; + } + } + } + for (uint32_t i = 0; i < table->count; i++) { + UNSERIALIZE_STR(table->entries[i].name); + table->entries[i].type_ref = + (new_nwa && i < new_nwa->count) ? &new_nwa->args[i] : NULL; + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_file_cache_unserialize_type(&table->entries[i].owned_type, scope, script, buf); + } } } @@ -1967,6 +2080,10 @@ static void zend_file_cache_unserialize_class(zval *zv, zend_file_cache_unserialize_generic_type_table(&ce->generic_types, ce, script, buf); } + if (ce->generic_type_args) { + zend_file_cache_unserialize_type_arg_table(&ce->generic_type_args, ce, script, buf); + } + zend_file_cache_unserialize_hash(&ce->properties_info, script, buf, zend_file_cache_unserialize_prop_info, NULL); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index d34553a48289..a31e7ec42f5f 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -375,6 +375,16 @@ static void zend_persist_type(zend_type *type) { if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + /* Compile-time NWAs can be structurally shared across the script + * (e.g. two `extends Foo, ...>` clauses end up referencing + * the same `Box` payload). Use the xlat-aware variant so the + * second visit short-circuits to the already-persisted SHM copy + * instead of memdup'ing from the now-freed heap source. */ + zend_type_named_with_args *prev = zend_shared_alloc_get_xlat_entry(named); + if (prev) { + ZEND_TYPE_SET_PTR(*type, prev); + return; + } named = zend_shared_memdup_put_free(named, ZEND_TYPE_NAMED_WITH_ARGS_SIZE(named->count)); if (named->name) { zend_accel_store_interned_string(named->name); @@ -478,6 +488,97 @@ static HashTable *zend_persist_generic_type_table_ht(HashTable *ht) return ptr; } +/* Persist the turbofish_args HT. Each entry is a zend_turbofish_args_entry — + * args_box is serialized to SHM, but cached_table/cache_key are runtime-only + * (per-process call cache) and must be reset to NULL/0 on the persisted copy + * so a fresh worker starts cold. */ +static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) +{ + zend_hash_persist(ht); + if (HT_IS_PACKED(ht)) { + zval *v; + ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { + zend_turbofish_args_entry *entry = Z_PTR_P(v); + zend_turbofish_args_entry *copy = zend_shared_memdup_put_free(entry, sizeof(*entry)); + zend_persist_type(©->args_box); + copy->cached_table = NULL; + copy->cache_key = 0; + Z_PTR_P(v) = copy; + } ZEND_HASH_FOREACH_END(); + } else { + Bucket *p; + ZEND_HASH_MAP_FOREACH_BUCKET(ht, p) { + if (p->key) { + zend_accel_store_interned_string(p->key); + } + zend_turbofish_args_entry *entry = Z_PTR(p->val); + zend_turbofish_args_entry *copy = zend_shared_memdup_put_free(entry, sizeof(*entry)); + zend_persist_type(©->args_box); + copy->cached_table = NULL; + copy->cache_key = 0; + Z_PTR(p->val) = copy; + } ZEND_HASH_FOREACH_END(); + } + HashTable *ptr = zend_shared_memdup_put_free(ht, sizeof(HashTable)); + GC_SET_REFCOUNT(ptr, 2); + GC_TYPE_INFO(ptr) = GC_ARRAY | ((IS_ARRAY_IMMUTABLE|GC_NOT_COLLECTABLE) << GC_FLAGS_SHIFT); + return ptr; +} + +/* Locate the post-persistence NWA payload that holds this monomorph's + * bindings — `extends` for class monos, the first `implements` entry for + * interface monos, the first `trait_uses` entry for trait monos. Returns + * NULL when the ce isn't a monomorph (no generic_types side table). */ +static const zend_type_named_with_args *zend_persist_mono_binding_nwa(const zend_class_entry *ce) +{ + if (!ce->generic_types) { + return NULL; + } + if (ce->generic_types->extends && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + return ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + } + HashTable *tables[] = { ce->generic_types->implements, ce->generic_types->trait_uses }; + for (size_t k = 0; k < sizeof(tables) / sizeof(tables[0]); k++) { + if (!tables[k] || zend_hash_num_elements(tables[k]) == 0) continue; + zval *zv; + ZEND_HASH_FOREACH_VAL(tables[k], zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (boxed && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + return ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + } + break; + } ZEND_HASH_FOREACH_END(); + } + return NULL; +} + +/* Persist a monomorph's generic_type_args. Each entry's `type_ref` was + * borrowed from a heap NWA payload that has just been relocated into SHM + * as part of the surrounding ce->generic_types; rebuild type_ref against + * the SHM payload so it stays valid after the heap source is freed. */ +static zend_type_arg_table *zend_persist_type_arg_table( + zend_type_arg_table *table, const zend_class_entry *ce) +{ + if (!table) { + return NULL; + } + const zend_type_named_with_args *new_nwa = zend_persist_mono_binding_nwa(ce); + for (uint32_t i = 0; i < table->count; i++) { + if (table->entries[i].name) { + zend_accel_store_interned_string(table->entries[i].name); + } + table->entries[i].type_ref = + (new_nwa && i < new_nwa->count) ? &new_nwa->args[i] : NULL; + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_persist_type(&table->entries[i].owned_type); + } + } + zend_type_arg_table *persisted = zend_shared_memdup_put_free( + table, ZEND_TYPE_ARG_TABLE_SIZE(table->count)); + persisted->persisted = true; + return persisted; +} + static zend_generic_type_table *zend_persist_generic_type_table(zend_generic_type_table *table) { if (!table) { @@ -517,7 +618,7 @@ static zend_generic_type_table *zend_persist_generic_type_table(zend_generic_typ } if (persisted->turbofish_args) { - persisted->turbofish_args = zend_persist_generic_type_table_ht(persisted->turbofish_args); + persisted->turbofish_args = zend_persist_turbofish_args_ht(persisted->turbofish_args); } return persisted; @@ -1350,6 +1451,10 @@ zend_class_entry *zend_persist_class_entry(zend_class_entry *orig_ce) ce->generic_types = zend_persist_generic_type_table(ce->generic_types); } + if (ce->generic_type_args) { + ce->generic_type_args = zend_persist_type_arg_table(ce->generic_type_args, ce); + } + if (ce->num_interfaces && !(ce->ce_flags & ZEND_ACC_LINKED)) { uint32_t i = 0; diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index b894640b5383..7a099207ea8b 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -290,6 +290,31 @@ static void zend_persist_generic_type_table_ht_calc(HashTable *ht) ADD_SIZE(sizeof(HashTable)); } +/* The turbofish_args HT entries are zend_turbofish_args_entry — same as a + * boxed type for size accounting purposes, but the entry struct itself is + * what we shared-memdup, not a bare zend_type. */ +static void zend_persist_turbofish_args_ht_calc(HashTable *ht) +{ + zend_hash_persist_calc(ht); + if (HT_IS_PACKED(ht)) { + zval *v; + ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { + ADD_SIZE(sizeof(zend_turbofish_args_entry)); + zend_persist_type_calc(&((zend_turbofish_args_entry *) Z_PTR_P(v))->args_box); + } ZEND_HASH_FOREACH_END(); + } else { + Bucket *p; + ZEND_HASH_MAP_FOREACH_BUCKET(ht, p) { + if (p->key) { + ADD_INTERNED_STRING(p->key); + } + ADD_SIZE(sizeof(zend_turbofish_args_entry)); + zend_persist_type_calc(&((zend_turbofish_args_entry *) Z_PTR(p->val))->args_box); + } ZEND_HASH_FOREACH_END(); + } + ADD_SIZE(sizeof(HashTable)); +} + static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) { if (!table) { @@ -328,7 +353,23 @@ static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) } if (table->turbofish_args) { - zend_persist_generic_type_table_ht_calc(table->turbofish_args); + zend_persist_turbofish_args_ht_calc(table->turbofish_args); + } +} + +static void zend_persist_type_arg_table_calc(zend_type_arg_table *table) +{ + if (!table) { + return; + } + ADD_SIZE(ZEND_TYPE_ARG_TABLE_SIZE(table->count)); + for (uint32_t i = 0; i < table->count; i++) { + if (table->entries[i].name) { + ADD_INTERNED_STRING(table->entries[i].name); + } + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_persist_type_calc(&table->entries[i].owned_type); + } } } @@ -736,6 +777,7 @@ void zend_persist_class_entry_calc(zend_class_entry *ce) zend_persist_generic_parameter_list_calc(ce->generic_parameters); zend_persist_generic_type_table_calc(ce->generic_types); + zend_persist_type_arg_table_calc(ce->generic_type_args); if (ce->num_interfaces) { uint32_t i; From 187aa1ff48e72d3f9fc4b7f8569e165032b373df Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 20 May 2026 23:38:06 +0200 Subject: [PATCH 10/30] optimize turbofish Signed-off-by: Robert Landers --- Zend/Optimizer/compact_literals.c | 12 ++++++++++ Zend/zend_compile.c | 40 +++++++++++++++---------------- Zend/zend_compile.h | 22 +++++++---------- Zend/zend_opcode.c | 17 ++++--------- Zend/zend_vm_def.h | 6 +++-- Zend/zend_vm_execute.h | 24 ++++++++++++------- ext/opcache/zend_file_cache.c | 10 +++----- ext/opcache/zend_persist.c | 12 ++++------ 8 files changed, 73 insertions(+), 70 deletions(-) diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c index cf6f39f31019..04345bd1c7ea 100644 --- a/Zend/Optimizer/compact_literals.c +++ b/Zend/Optimizer/compact_literals.c @@ -694,6 +694,18 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx } } break; + case ZEND_VERIFY_GENERIC_ARGUMENTS: + case ZEND_INSTALL_GENERIC_ARGS: + /* When this site has a turbofish (args_id in extended_value), + * the compiler allocated a 2-slot inline cache for the + * (zend_type_arg_table*, cache key) pair — re-allocate it + * here so the offset stays in sync with compact_literals' + * fresh cache_size. */ + if (opline->extended_value != 0) { + opline->result.num = cache_size; + cache_size += 2 * sizeof(void *); + } + break; case ZEND_CATCH: if (opline->op1_type == IS_CONST) { // op1 class diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 2e8736ba9108..165cfb6dbf8c 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -715,34 +715,28 @@ static bool zend_call_is_cacheable_against_args( * stable, the freshly built table is also stashed in the cache for future * iterations. The "store on first miss, never replace" policy avoids * dangling: a replaced cached_table could still be referenced by a live - * frame's call->type_args. */ + * frame's call->type_args. + * + * `cache_slot` points to a 2-pointer runtime cache slot owned by the + * executing op_array — slot[0] = cached zend_type_arg_table*, slot[1] = + * cache key. Runtime cache memory is allocated per-process and writable + * even when the op_array's persistent metadata lives in opcache SHM. */ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( - zend_execute_data *call, zend_turbofish_args_entry *entry) + zend_execute_data *call, const zend_type *args_box, void **cache_slot) { - if (!entry) { + if (!args_box) { return zend_build_generic_call_type_args(call, NULL); } - const zend_type *args_box = &entry->args_box; zend_execute_data *caller = EG(current_execute_data); - /* The entry lives in the caller op_array's generic_types side table; when - * that side table has been persisted to opcache SHM it's mapped read-only, - * so any cache writes here would SIGSEGV. The `persisted` flag on the - * generic_type_table is set by zend_persist_generic_type_table and is the - * authoritative signal (op_array->fn_flags ZEND_ACC_IMMUTABLE is unset on - * $_main even though its side table is still in SHM). */ - bool entry_writable = !caller || !caller->func - || !ZEND_USER_CODE(caller->func->common.type) - || !caller->func->op_array.generic_types - || !caller->func->op_array.generic_types->persisted; uintptr_t key = zend_compute_call_cache_key(args_box, caller); - if (key && entry->cached_table && entry->cache_key == key) { - return entry->cached_table; + if (cache_slot && key && cache_slot[0] && (uintptr_t)cache_slot[1] == key) { + return (zend_type_arg_table *)cache_slot[0]; } zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); - if (t && key && entry_writable && !entry->cached_table + if (t && key && cache_slot && !cache_slot[0] && zend_call_is_cacheable_against_args(call->func, args_box)) { - entry->cached_table = t; - entry->cache_key = key; + cache_slot[0] = t; + cache_slot[1] = (void *)key; t->persisted = true; } return t; @@ -1207,7 +1201,13 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t opline->op1.num = 0; } opline->result_type = IS_UNUSED; - opline->result.num = 0; + /* When there's a turbofish, reserve a 2-slot inline cache in the caller + * op_array's runtime cache: slot[0] = cached zend_type_arg_table*, + * slot[1] = caller-binding fingerprint (cache key). The runtime cache is + * per-process and writable even when the op_array's side tables are + * persisted to opcache SHM, so this cache survives opcache without the + * SHM-write gating that previously disabled it. */ + opline->result.num = args_id ? zend_alloc_cache_slots(2) : 0; } static zend_generic_parameter_list *zend_compile_generic_type_parameter_list( diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 12b6ae61dc20..785b2708b42b 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -245,29 +245,25 @@ static zend_always_inline const zend_type *zend_type_arg_entry_type(const zend_t return NULL; } -/* Per-call-site side entry: stores both the compile-time args_box and a - * one-slot runtime cache of the most recently built type-arg table at this - * call site, keyed on the caller frame's binding identities. On a hit, the - * cached table is reused (marked persisted so the frame doesn't free it) - * instead of rebuilding via zend_build_generic_call_type_args. Inline - * args_box keeps zend_generic_get_turbofish_args returning a stable pointer - * at the same offset that callers expect. */ +/* Per-call-site side entry: stores the compile-time args_box. The one-slot + * runtime cache that pairs with this (cached zend_type_arg_table* keyed on + * the caller's binding identities) lives in the caller op_array's runtime + * cache, addressed by opline->result.num — that storage is per-process and + * writable even when the entry itself is persisted to opcache SHM. */ typedef struct _zend_turbofish_args_entry { zend_type args_box; - zend_type_arg_table *cached_table; - /* 0 = empty; ZEND_TURBOFISH_CACHE_KEY_CONCRETE = invariant (no T-refs); - * otherwise a fingerprint built from the caller's referenced binding name - * pointers (interned, so pointer-stable across calls with same value). */ - uintptr_t cache_key; } zend_turbofish_args_entry; +/* Cache key sentinel for concrete-arg call sites (no T-refs in the args). + * Cache key 0 means empty; CONCRETE means "args fully resolved at compile + * time, table is invariant across calls." */ #define ZEND_TURBOFISH_CACHE_KEY_CONCRETE ((uintptr_t)1) ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count); ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); -ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args(zend_execute_data *call, struct _zend_turbofish_args_entry *entry); +ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args(zend_execute_data *call, const zend_type *args_box, void **cache_slot); ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type); ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, uint32_t fetch_type); ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 9dcafdaa1d1a..97f0cc7d1d0e 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -180,18 +180,13 @@ static void zend_generic_type_table_value_dtor(zval *zv) { efree(type); } -/* Dedicated dtor for turbofish_args entries: each one owns both an inline - * args_box (released like a normal boxed type) and a one-slot cached_table - * for the runtime call-cache, which must be torn down here since the cache - * survives across calls and isn't tied to any single frame. */ +/* Dtor for turbofish_args entries: each one owns an inline args_box. + * The per-call-site runtime cache that stashes the most recently built + * zend_type_arg_table for this entry lives in the caller op_array's + * runtime cache slot, so it's released by the op_array runtime-cache + * teardown — not here. */ static void zend_turbofish_args_entry_dtor(zval *zv) { zend_turbofish_args_entry *entry = Z_PTR_P(zv); - if (entry->cached_table) { - /* Drop the persisted flag so the destroy path actually releases names - * and frees the table — the cache, not opcache SHM, owns this. */ - entry->cached_table->persisted = false; - zend_type_arg_table_destroy(entry->cached_table); - } zend_type_release(entry->args_box, /* persistent */ false); efree(entry); } @@ -316,8 +311,6 @@ ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type) { zend_turbofish_args_entry *entry = emalloc(sizeof(*entry)); entry->args_box = type; - entry->cached_table = NULL; - entry->cache_key = 0; zend_hash_index_update_ptr(zend_generic_type_table_ensure_turbofish(&t->turbofish_args), op_num, entry); } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 6d7e841bad76..b2193b2986f3 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9026,6 +9026,7 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) uint32_t arity = opline->op2.num; zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); @@ -9042,7 +9043,7 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -9110,11 +9111,12 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) zend_execute_data *call = EX(call); zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); if (OP1_TYPE == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 4c1bd41b673a..824982c818dd 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -22134,6 +22134,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI uint32_t arity = opline->op2.num; zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); @@ -22150,7 +22151,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -22218,11 +22219,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER zend_execute_data *call = EX(call); zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -37739,6 +37741,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI uint32_t arity = opline->op2.num; zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); @@ -37755,7 +37758,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -37823,11 +37826,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER zend_execute_data *call = EX(call); zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -75244,6 +75248,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG uint32_t arity = opline->op2.num; zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); @@ -75260,7 +75265,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -75328,11 +75333,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR zend_execute_data *call = EX(call); zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -90849,6 +90855,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG uint32_t arity = opline->op2.num; zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); @@ -90865,7 +90872,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } zend_check_generic_call_arguments(call->func, arity, args_box); if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); @@ -90933,11 +90940,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR zend_execute_data *call = EX(call); zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; + void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, call_entry); + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); if (t) { if (call->type_args) { zend_type_arg_table_destroy(call->type_args); diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index 09e0d6347960..4827a4dcd0a3 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -535,9 +535,9 @@ static void zend_file_cache_serialize_generic_type_entry( } /* Serialise a turbofish_args entry — same shape as a boxed type from the - * file's perspective but the buffer slot is the larger entry struct. Runtime - * cache fields (cached_table, cache_key) are not persisted; the unserialize - * side resets them to a cold state. */ + * file's perspective but the buffer slot is the entry struct (currently a + * single args_box; the per-call-site runtime cache that pairs with it lives + * in the caller op_array's runtime cache slot, not here). */ static void zend_file_cache_serialize_turbofish_args_entry( zval *zv, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) { @@ -545,8 +545,6 @@ static void zend_file_cache_serialize_turbofish_args_entry( zend_turbofish_args_entry *entry = Z_PTR_P(zv); UNSERIALIZE_PTR(entry); zend_file_cache_serialize_type(&entry->args_box, script, info, buf); - entry->cached_table = NULL; - entry->cache_key = 0; } static void zend_file_cache_serialize_generic_type_table_ht( @@ -1629,8 +1627,6 @@ static void zend_file_cache_unserialize_turbofish_args_entry( zend_turbofish_args_entry *entry = Z_PTR_P(zv); zend_file_cache_unserialize_type( &entry->args_box, zend_file_cache_generic_unserialize_scope, script, buf); - entry->cached_table = NULL; - entry->cache_key = 0; } static void zend_file_cache_unserialize_generic_type_table_ht( diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index a31e7ec42f5f..754509c2d108 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -488,10 +488,10 @@ static HashTable *zend_persist_generic_type_table_ht(HashTable *ht) return ptr; } -/* Persist the turbofish_args HT. Each entry is a zend_turbofish_args_entry — - * args_box is serialized to SHM, but cached_table/cache_key are runtime-only - * (per-process call cache) and must be reset to NULL/0 on the persisted copy - * so a fresh worker starts cold. */ +/* Persist the turbofish_args HT. Each entry is a zend_turbofish_args_entry + * which now stores only args_box — the per-call-site runtime cache that + * pairs with it lives in the caller op_array's runtime cache slot, not + * here, so the persisted SHM copy carries no per-process state. */ static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) { zend_hash_persist(ht); @@ -501,8 +501,6 @@ static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) zend_turbofish_args_entry *entry = Z_PTR_P(v); zend_turbofish_args_entry *copy = zend_shared_memdup_put_free(entry, sizeof(*entry)); zend_persist_type(©->args_box); - copy->cached_table = NULL; - copy->cache_key = 0; Z_PTR_P(v) = copy; } ZEND_HASH_FOREACH_END(); } else { @@ -514,8 +512,6 @@ static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) zend_turbofish_args_entry *entry = Z_PTR(p->val); zend_turbofish_args_entry *copy = zend_shared_memdup_put_free(entry, sizeof(*entry)); zend_persist_type(©->args_box); - copy->cached_table = NULL; - copy->cache_key = 0; Z_PTR(p->val) = copy; } ZEND_HASH_FOREACH_END(); } From 1c9d82410c20f90e1253c0467380410d47778774 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 22 May 2026 22:36:16 +0200 Subject: [PATCH 11/30] so many fixes... Signed-off-by: Robert Landers --- .../namespace_visibility_parser_test.phpt | 38 -- Zend/tests/exit/exit_statements.phpt | 4 +- .../self_ref_through_generic_arg_allowed.phpt | 13 +- .../erasure/instanceof_args_discarded.phpt | 10 +- .../generics/erasure/named_args_stripped.phpt | 10 +- .../type_args_on_non_generic_catch.phpt | 12 + .../type_args_on_non_generic_implements.phpt | 9 + .../type_args_on_non_generic_instanceof.phpt | 10 + .../errors/type_args_on_non_generic_new.phpt | 9 + .../type_args_on_non_generic_param.phpt | 9 + .../type_args_on_non_generic_property.phpt | 11 + .../type_args_on_non_generic_return.phpt | 9 + .../arity/extends_non_generic.phpt | 2 +- ...iant_interface_diamond_still_rejected.phpt | 3 +- .../reification/closure_bind_preserves_T.phpt | 53 +++ .../reification/closure_call_preserves_T.phpt | 57 +++ .../reification/closure_captures_T.phpt | 43 +- .../closure_from_callable_preserves_T.phpt | 27 ++ .../reification/property_t_reified.phpt | 64 +++ .../reification/return_type_reified.phpt | 63 +++ .../return_type_reified_strict.phpt | 44 ++ .../reification/union_with_t_in_property.phpt | 63 +++ .../reification/variadic_t_in_closure.phpt | 55 +++ .../reification/variadic_t_reified.phpt | 75 +++ .../reification/variance_reified.phpt | 54 +++ .../variance_reified_violation_param.phpt | 15 + .../variance_reified_violation_return.phpt | 15 + .../variance_reified_violation_widening.phpt | 15 + .../monomorph_unserialize_fresh_process.phpt | 63 +++ .../generics/scoping/trait_use_clause.phpt | 21 +- Zend/tests/generics/syntax/args_dnf.phpt | 2 +- .../generics/syntax/args_intersection.phpt | 2 +- Zend/tests/generics/syntax/args_union.phpt | 2 +- .../syntax/bound_and_default_with_args.phpt | 8 +- .../generics/syntax/catch_with_args.phpt | 21 +- .../syntax/default_with_nested_args.phpt | 4 +- .../generics/syntax/instanceof_with_args.phpt | 12 +- .../generics/syntax/named_type_args.phpt | 6 +- .../generics/syntax/nested_three_levels.phpt | 10 +- .../generics/syntax/nested_two_levels.phpt | 8 +- .../generics/syntax/trailing_comma_args.phpt | 2 +- .../generics/turbofish/arity/new_arity.phpt | 5 - .../turbofish/instanceof_with_args.phpt | 7 - Zend/zend_closures.c | 51 +- Zend/zend_closures.h | 14 + Zend/zend_compile.c | 438 ++++++++++++++---- Zend/zend_compile.h | 2 + Zend/zend_execute.h | 14 +- Zend/zend_execute_API.c | 20 + Zend/zend_inheritance.c | 206 +++++++- Zend/zend_inheritance.h | 1 + Zend/zend_opcode.c | 62 +++ Zend/zend_vm_def.h | 58 ++- Zend/zend_vm_execute.h | 392 +++++++++++++++- bench/bench_reification.php | 415 +++++++++++++++++ ext/opcache/jit/zend_jit_ir.c | 9 +- ext/standard/var_unserializer.re | 12 +- 57 files changed, 2389 insertions(+), 270 deletions(-) delete mode 100644 Zend/tests/access_modifiers/namespace_visibility_parser_test.phpt create mode 100644 Zend/tests/generics/errors/type_args_on_non_generic_catch.phpt create mode 100644 Zend/tests/generics/errors/type_args_on_non_generic_implements.phpt create mode 100644 Zend/tests/generics/errors/type_args_on_non_generic_instanceof.phpt create mode 100644 Zend/tests/generics/errors/type_args_on_non_generic_new.phpt create mode 100644 Zend/tests/generics/errors/type_args_on_non_generic_param.phpt create mode 100644 Zend/tests/generics/errors/type_args_on_non_generic_property.phpt create mode 100644 Zend/tests/generics/errors/type_args_on_non_generic_return.phpt create mode 100644 Zend/tests/generics/reification/closure_bind_preserves_T.phpt create mode 100644 Zend/tests/generics/reification/closure_call_preserves_T.phpt create mode 100644 Zend/tests/generics/reification/closure_from_callable_preserves_T.phpt create mode 100644 Zend/tests/generics/reification/property_t_reified.phpt create mode 100644 Zend/tests/generics/reification/return_type_reified.phpt create mode 100644 Zend/tests/generics/reification/return_type_reified_strict.phpt create mode 100644 Zend/tests/generics/reification/union_with_t_in_property.phpt create mode 100644 Zend/tests/generics/reification/variadic_t_in_closure.phpt create mode 100644 Zend/tests/generics/reification/variadic_t_reified.phpt create mode 100644 Zend/tests/generics/reification/variance_reified.phpt create mode 100644 Zend/tests/generics/reification/variance_reified_violation_param.phpt create mode 100644 Zend/tests/generics/reification/variance_reified_violation_return.phpt create mode 100644 Zend/tests/generics/reification/variance_reified_violation_widening.phpt create mode 100644 Zend/tests/generics/runtime/monomorph_unserialize_fresh_process.phpt diff --git a/Zend/tests/access_modifiers/namespace_visibility_parser_test.phpt b/Zend/tests/access_modifiers/namespace_visibility_parser_test.phpt deleted file mode 100644 index 9838a951db49..000000000000 --- a/Zend/tests/access_modifiers/namespace_visibility_parser_test.phpt +++ /dev/null @@ -1,38 +0,0 @@ ---TEST-- -Parser can distinguish namespace declaration from namespace visibility modifier ---FILE-- -helper(); -$session->count++; -echo "Count: {$session->count}\n"; - -// Test 3: Different namespace -namespace App\Other; - -$session2 = new \App\Auth\SessionManager(); -try { - $session2->helper(); - echo "ERROR: Should have thrown\n"; -} catch (\Error $e) { - echo "Correctly blocked: namespace visibility works\n"; -} - -?> ---EXPECT-- -Helper called -Count: 1 -Correctly blocked: namespace visibility works diff --git a/Zend/tests/exit/exit_statements.phpt b/Zend/tests/exit/exit_statements.phpt index 6b3147156709..c64b6bedda94 100644 --- a/Zend/tests/exit/exit_statements.phpt +++ b/Zend/tests/exit/exit_statements.phpt @@ -17,7 +17,9 @@ TEMPLATE; $php = getenv('TEST_PHP_EXECUTABLE_ESCAPED'); -$command = $php . ' ' . escapeshellarg(FILE_PATH); +// Use -n so the spawned PHP doesn't inherit host php.ini extension +// configuration (otherwise missing extension warnings can pollute output). +$command = $php . ' -n ' . escapeshellarg(FILE_PATH); foreach (['exit', 'die'] as $value) { echo 'Using ', $value, ' as value:', PHP_EOL; diff --git a/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt b/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt index b50f0f59404e..ee81760af212 100644 --- a/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt +++ b/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt @@ -1,10 +1,17 @@ --TEST-- -Recursive bounds: self-reference inside a generic type argument is allowed (pre-existing behavior) +Recursive bounds: F-bounded polymorphism (T appears as a type argument inside its own bound) --FILE-- ` — T isn't its own bound, it's a type +// *argument* to the bound. This is the pattern PHP supports for self-referential +// constraints (analogous to Java `>`). class Box {} class Foo> {} -echo "ok\n"; + +// A direct self-bound (`T: T`) would be rejected; verify the F-bounded form +// is in fact distinct by reflecting it. +$bound = (new ReflectionClass('Foo'))->getGenericParameters()[0]; +echo $bound, "\n"; ?> --EXPECT-- -ok +T : Box diff --git a/Zend/tests/generics/erasure/instanceof_args_discarded.phpt b/Zend/tests/generics/erasure/instanceof_args_discarded.phpt index f97188fcae52..d2a501d96193 100644 --- a/Zend/tests/generics/erasure/instanceof_args_discarded.phpt +++ b/Zend/tests/generics/erasure/instanceof_args_discarded.phpt @@ -1,14 +1,10 @@ --TEST-- -Erasure: instanceof on a non-generic class with type arguments resolves to the canonical name (which does not exist) and returns false +Erasure: instanceof on a non-generic class with type arguments is a compile-time error --FILE-- ); -var_dump($c instanceof C); ?> ---EXPECT-- -bool(true) -bool(false) -bool(false) +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class C in %s on line %d diff --git a/Zend/tests/generics/erasure/named_args_stripped.phpt b/Zend/tests/generics/erasure/named_args_stripped.phpt index 437580417c71..066e6680ad72 100644 --- a/Zend/tests/generics/erasure/named_args_stripped.phpt +++ b/Zend/tests/generics/erasure/named_args_stripped.phpt @@ -1,13 +1,9 @@ --TEST-- -Erasure: type arguments stripped from named types in runtime view +Erasure: type arguments on a non-generic class in a parameter/return type is a compile-time error --FILE-- $x): Container { return $x; } -$r = new ReflectionFunction('f'); -echo $r->getParameters()[0]->getType()->getName(), "\n"; -echo $r->getReturnType()->getName(), "\n"; ?> ---EXPECT-- -Container -Container +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Container in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_catch.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_catch.phpt new file mode 100644 index 000000000000..48bbd541521a --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_catch.phpt @@ -0,0 +1,12 @@ +--TEST-- +Type arguments on a non-generic class in `catch` are a compile-time error +--FILE-- + $e) { +} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class MyErr in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_implements.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_implements.phpt new file mode 100644 index 000000000000..ec535974e86f --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_implements.phpt @@ -0,0 +1,9 @@ +--TEST-- +Type arguments on a non-generic interface in `implements` are a compile-time error +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_instanceof.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_instanceof.phpt new file mode 100644 index 000000000000..fe06732eed73 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_instanceof.phpt @@ -0,0 +1,10 @@ +--TEST-- +Type arguments on a non-generic class in `instanceof` are a compile-time error +--FILE-- +); +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_new.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_new.phpt new file mode 100644 index 000000000000..2490ce2181cc --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_new.phpt @@ -0,0 +1,9 @@ +--TEST-- +Type arguments on a non-generic class in `new` (turbofish) are a compile-time error +--FILE-- +(); +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_param.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_param.phpt new file mode 100644 index 000000000000..d23d51ad3c49 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_param.phpt @@ -0,0 +1,9 @@ +--TEST-- +Type arguments on a non-generic class in a parameter type are a compile-time error +--FILE-- + $x): void {} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_property.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_property.phpt new file mode 100644 index 000000000000..49e1e7356012 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_property.phpt @@ -0,0 +1,11 @@ +--TEST-- +Type arguments on a non-generic class in a property type are a compile-time error +--FILE-- + $x; +} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_return.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_return.phpt new file mode 100644 index 000000000000..7a4b5d3d19b0 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_return.phpt @@ -0,0 +1,9 @@ +--TEST-- +Type arguments on a non-generic class in a return type are a compile-time error +--FILE-- + { return new Plain; } +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt b/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt index c4d213c2c4d4..79b2bfa12def 100644 --- a/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt +++ b/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt @@ -6,4 +6,4 @@ class Plain {} class Bad extends Plain {} ?> --EXPECTF-- -Fatal error: Too many generic type arguments to extends Plain in Bad, 1 passed and exactly 0 expected in %s on line %d +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt index aa460a7a88c3..61476796c8a7 100644 --- a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt @@ -5,7 +5,6 @@ Diamond + invariant T: arity mismatch is the one remaining rejection at the diam interface Multi {} interface Alpha extends Multi {} interface Beta extends Multi {} -PHP ?> --EXPECTF-- -Fatal error: %s on line %d +Fatal error: Too many generic type arguments to extends Multi in Beta, 3 passed and exactly 2 expected in %s on line %d diff --git a/Zend/tests/generics/reification/closure_bind_preserves_T.phpt b/Zend/tests/generics/reification/closure_bind_preserves_T.phpt new file mode 100644 index 000000000000..ce3381902407 --- /dev/null +++ b/Zend/tests/generics/reification/closure_bind_preserves_T.phpt @@ -0,0 +1,53 @@ +--TEST-- +Reification: Closure::bind / Closure::bindTo carry the captured T-table to the rebound closure +--FILE-- +(): Closure { + return function (T $x): T { return $x; }; +} + +$f = maker::(); + +// Closure::bindTo to a new instance of A (compatible) — same scope. +$rb = $f->bindTo(new A); +var_dump(get_class($rb(new A))); // accepts A + +try { + $rb(new B); // wrong concrete class +} catch (TypeError $e) { + echo "bindTo wrong: ", $e->getMessage(), "\n"; +} + +// Closure::bind static form — also propagates. +$rb2 = Closure::bind($f, new A); +var_dump(get_class($rb2(new A))); + +try { + $rb2(new B); +} catch (TypeError $e) { + echo "bind wrong: ", $e->getMessage(), "\n"; +} + +// bindTo(null) — strip the $this — still keeps the T-table. +$rb3 = $f->bindTo(null); +var_dump(get_class($rb3(new A))); +try { + $rb3(new B); +} catch (TypeError $e) { + echo "bindTo(null) wrong: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +string(1) "A" +bindTo wrong: %s must be of type A, B given%S +string(1) "A" +bind wrong: %s must be of type A, B given%S +string(1) "A" +bindTo(null) wrong: %s must be of type A, B given%S diff --git a/Zend/tests/generics/reification/closure_call_preserves_T.phpt b/Zend/tests/generics/reification/closure_call_preserves_T.phpt new file mode 100644 index 000000000000..b94ab23d3682 --- /dev/null +++ b/Zend/tests/generics/reification/closure_call_preserves_T.phpt @@ -0,0 +1,57 @@ +--TEST-- +Reification: Closure::call propagates the captured T-table into the fake closure that actually runs the body +--FILE-- +(): Closure { + return function (T $x): string { + $this->store = $x; + return get_class($x); + }; +} + +$f = maker::(); +$h = new Holder; + +// Positive: A and its subclass B both satisfy T = A. +var_dump($f->call($h, new A)); +var_dump($f->call($h, new B)); + +// Negative: a value that doesn't satisfy T fires with the resolved T. +class Unrelated {} +try { + $f->call($h, new Unrelated); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} + +// Variadic + call: every element checked against the captured T. +function vmaker(): Closure { + return function (T ...$xs): int { return count($xs); }; +} + +$vf = vmaker::(); +var_dump($vf->call($h, new A, new B, new A)); + +try { + $vf->call($h, new A, new Unrelated, new A); +} catch (TypeError $e) { + echo "rejected2: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +string(1) "A" +string(1) "B" +rejected: %s must be of type A, Unrelated given%S +int(3) +rejected2: %s must be of type A, Unrelated given%S diff --git a/Zend/tests/generics/reification/closure_captures_T.phpt b/Zend/tests/generics/reification/closure_captures_T.phpt index 60ff11ed5d5a..d53c5a2fc5a1 100644 --- a/Zend/tests/generics/reification/closure_captures_T.phpt +++ b/Zend/tests/generics/reification/closure_captures_T.phpt @@ -1,21 +1,44 @@ --TEST-- -Reification: a closure returned from a generic function captures its T via an eagerly-resolved variable +Reification: a closure created inside a generic frame captures that frame's T-table, so the closure's body resolves T to the binding in effect at creation time --FILE-- (): Closure { - $name = T::class; - return fn() => $name; +// T::class inside the body needs to resolve to the captured binding (no +// turbofish at the closure's own call site). +function nameOf(): Closure { + return fn() => T::class; } +echo nameOf::()(), "\n"; +echo nameOf::()(), "\n"; -echo fact::()(), "\n"; -echo fact::()(), "\n"; +// A closure with a T-typed parameter rejects values that don't satisfy the +// CAPTURED binding, not just the parameter's erased bound. +function id(): Closure { + return function(T $x): T { return $x; }; +} +$fooId = id::(); +$f = $fooId(new Foo); +var_dump(get_class($f)); + +try { + $fooId(new Bar); +} catch (TypeError $e) { + echo "rejected wrong class: ", $e->getMessage(), "\n"; +} + +// Scalars are rejected against the resolved T (Foo) — the captured binding +// is more specific than the parameter's erased object bound. +try { + $fooId(42); +} catch (TypeError $e) { + echo "rejected scalar: ", $e->getMessage(), "\n"; +} ?> ---EXPECT-- +--EXPECTF-- Foo Bar +string(3) "Foo" +rejected wrong class: %s must be of type Foo, Bar given%S +rejected scalar: %s must be of type Foo, int given%S diff --git a/Zend/tests/generics/reification/closure_from_callable_preserves_T.phpt b/Zend/tests/generics/reification/closure_from_callable_preserves_T.phpt new file mode 100644 index 000000000000..063b4ddc9dce --- /dev/null +++ b/Zend/tests/generics/reification/closure_from_callable_preserves_T.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: Closure::fromCallable on a generic closure preserves the captured T-table (returns the same closure) +--FILE-- +(): Closure { + return function (T $x): string { return get_class($x); }; +} + +$f = maker::(); +$wrapped = Closure::fromCallable($f); + +// Positive: behaves like the original — T is still A. +var_dump($wrapped(new A)); + +// Negative: wrong concrete class still fires with the resolved T. +try { + $wrapped(new B); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +string(1) "A" +rejected: %s must be of type A, B given%S diff --git a/Zend/tests/generics/reification/property_t_reified.phpt b/Zend/tests/generics/reification/property_t_reified.phpt new file mode 100644 index 000000000000..f8da7dfec662 --- /dev/null +++ b/Zend/tests/generics/reification/property_t_reified.phpt @@ -0,0 +1,64 @@ +--TEST-- +Reification: typed properties whose declared type is `T` check the reified binding on assignment +--FILE-- +() — assignments to $value must +// check against int, not the erased "mixed" bound. + +class Box { + public T $value; +} + +$b = new Box::(); +$b->value = 42; +var_dump($b->value); + +try { + $b->value = "not-an-int"; +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +// Object T case. +class Item {} +class Other {} + +class Holder { + public T $thing; +} + +$h = new Holder::(); +$h->thing = new Item; +var_dump(get_class($h->thing)); + +try { + $h->thing = new Other; +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +// Nullable T. +class MaybeBox { + public ?T $value = null; +} + +$mb = new MaybeBox::(); +$mb->value = 7; +var_dump($mb->value); +$mb->value = null; +var_dump($mb->value); + +try { + $mb->value = "wrong"; +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +int(42) +1: Cannot assign string to property Box::$value of type int +string(4) "Item" +2: Cannot assign Other to property Holder::$thing of type Item +int(7) +NULL +3: Cannot assign string to property MaybeBox::$value of type ?int diff --git a/Zend/tests/generics/reification/return_type_reified.phpt b/Zend/tests/generics/reification/return_type_reified.phpt new file mode 100644 index 000000000000..04f3ba7f0aeb --- /dev/null +++ b/Zend/tests/generics/reification/return_type_reified.phpt @@ -0,0 +1,63 @@ +--TEST-- +Reification: VERIFY_RETURN_TYPE checks the reified T binding, not the erased bound +--FILE-- +(T $x): T { return $x; } +function ret_nonnumeric_string(T $x): T { return "abc"; } +function ret_array(T $x): T { return [1, 2]; } +function maybe(T $x): ?T { return null; } +function lift(T $x): array { return [$x]; } + +// Positive: returning the correct concrete T passes. +var_dump(id::(42)); +var_dump(id::("hi")); + +// Negative: returning a wrong, non-coercible concrete type fires with the +// resolved T, not the erased "mixed". +// - "abc" (non-numeric string) cannot coerce to int even in weak mode. +// - array cannot coerce to string at all. +try { + ret_nonnumeric_string::(1); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} +try { + ret_array::("x"); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +// `?T`: nullable return on a T-typed function still accepts null when bound +// to a non-nullable concrete type. The reified check preserves the nullable +// bit from the outer T-ref. +var_dump(maybe::(0)); + +// Non-T return type (`array`) is unaffected by the reified-return path — +// goes through the normal erased check. +var_dump(lift::(7)); + +// Object T: returning the wrong class names the bound class in the error. +class A {} +class B {} +function obj(T $x): T { return new B(); } +try { + obj::(new A()); +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(2) "hi" +1: ret_nonnumeric_string(): Return value must be of type int, string returned +2: ret_array(): Return value must be of type string, array returned +NULL +array(1) { + [0]=> + int(7) +} +3: obj(): Return value must be of type A, B returned diff --git a/Zend/tests/generics/reification/return_type_reified_strict.phpt b/Zend/tests/generics/reification/return_type_reified_strict.phpt new file mode 100644 index 000000000000..16a9855f3fbe --- /dev/null +++ b/Zend/tests/generics/reification/return_type_reified_strict.phpt @@ -0,0 +1,44 @@ +--TEST-- +Reification: VERIFY_RETURN_TYPE under strict_types catches coercible mismatches the weak path would silently accept +--FILE-- +(T $x): T { return 999; } +function ret_str(T $x): T { return "1.5"; } +function ret_float(T $x): T { return 3.14; } + +// Under strict_types, an int return from a function bound to T=string must +// throw — weak mode would silently coerce to "999". +try { + ret_int::("a"); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +// Numeric string returned where T=int — weak would coerce; strict throws. +try { + ret_str::(0); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +// Float returned where T=int — weak would coerce; strict throws. +try { + ret_float::(0); +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} + +// Positive: correct concrete types still pass. +var_dump(ret_int::(0)); +var_dump(ret_str::("a")); +var_dump(ret_float::(0.0)); +?> +--EXPECTF-- +1: ret_int(): Return value must be of type string, int returned +2: ret_str(): Return value must be of type int, string returned +3: ret_float(): Return value must be of type int, float returned +int(999) +string(3) "1.5" +float(3.14) diff --git a/Zend/tests/generics/reification/union_with_t_in_property.phpt b/Zend/tests/generics/reification/union_with_t_in_property.phpt new file mode 100644 index 000000000000..2d594bbeaf0b --- /dev/null +++ b/Zend/tests/generics/reification/union_with_t_in_property.phpt @@ -0,0 +1,63 @@ +--TEST-- +Reification: a union `T|null` (and other unions containing T) on a property reifies the T-ref the same way `?T` does +--FILE-- + { + public T|null $w; + public function __construct(T|null $w) { $this->w = $w; } +} + +class TInt { + public T|int $w; + public function __construct(T|int $w) { $this->w = $w; } +} + +class Foo {} +class Bar {} +class Baz {} + +class TFoo { + public T|Foo $w; + public function __construct(T|Foo $w) { $this->w = $w; } +} + +// T|null with primitive T accepts T and null and rejects others. +$a = new TNull::(5); +var_dump($a->w); +$a->w = null; +var_dump($a->w); +try { $a->w = "x"; } catch (TypeError $e) { echo "1: ", $e->getMessage(), "\n"; } + +// T|int: T=string accepts strings and ints; under strict_types, a float is +// rejected outright. +$b = new TInt::("hi"); +var_dump($b->w); +$b->w = 7; +var_dump($b->w); +try { $b->w = 1.5; } catch (TypeError $e) { echo "2: ", $e->getMessage(), "\n"; } + +// T|Foo: T=Bar accepts Bar or Foo, rejects an unrelated class. +$c = new TFoo::(new Bar); +var_dump($c->w::class); +$c->w = new Foo; +var_dump($c->w::class); +try { $c->w = new Baz; } catch (TypeError $e) { echo "3: ", $e->getMessage(), "\n"; } +?> +--EXPECT-- +int(5) +NULL +1: Cannot assign string to property TNull::$w of type ?int +string(2) "hi" +int(7) +2: Cannot assign float to property TInt::$w of type string|int +string(3) "Bar" +string(3) "Foo" +3: Cannot assign Baz to property TFoo::$w of type Bar|Foo diff --git a/Zend/tests/generics/reification/variadic_t_in_closure.phpt b/Zend/tests/generics/reification/variadic_t_in_closure.phpt new file mode 100644 index 000000000000..696944ae10ea --- /dev/null +++ b/Zend/tests/generics/reification/variadic_t_in_closure.phpt @@ -0,0 +1,55 @@ +--TEST-- +Reification: variadic T parameter is checked even when T comes from a closure's captured frame, not the closure's own turbofish +--FILE-- +(): Closure { + return function(T ...$xs): int { + return count($xs); + }; +} + +$intMaker = maker::(); +var_dump($intMaker(1, 2, 3)); // positive: all int + +try { + $intMaker(1, "bad", 3); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +try { + $intMaker(1, 2, [4]); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +class Animal {} +class Dog extends Animal {} +class Cat extends Animal {} + +function dogMaker(): Closure { + return function(T ...$xs): int { + return count($xs); + }; +} + +$dm = dogMaker::(); +var_dump($dm(new Dog, new Dog)); + +try { + $dm(new Dog, new Cat); +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(3) +1: {closure:maker():%d}(): Argument #2 ($xs) must be of type int, string given +2: {closure:maker():%d}(): Argument #3 ($xs) must be of type int, array given +int(2) +3: {closure:dogMaker():%d}(): Argument #2 ($xs) must be of type Dog, Cat given diff --git a/Zend/tests/generics/reification/variadic_t_reified.phpt b/Zend/tests/generics/reification/variadic_t_reified.phpt new file mode 100644 index 000000000000..87b01a3001d0 --- /dev/null +++ b/Zend/tests/generics/reification/variadic_t_reified.phpt @@ -0,0 +1,75 @@ +--TEST-- +Reification: variadic T parameter checks every element against the reified T binding, not just the first +--FILE-- +(T ...$xs): T { + $r = $xs[0]; + for ($i = 1; $i < count($xs); $i++) $r += $xs[$i]; + return $r; +} + +// Positive: all three elements are ints. +var_dump(sum::(1, 2, 3)); + +// Negative: the second element violates T; the error names that element +// (Argument #2) with the resolved T type ("int"). +try { + sum::(1, "abc", 3); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +// Negative: the third element violates T. +try { + sum::(1, 2, "xyz"); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +// Variadic with leading non-variadic param: positional 1 ($prefix) is a +// plain string, the variadic slot is `T ...`. The reified sweep starts at +// the variadic position and reports element index correctly. +function concat(string $prefix, T ...$xs): string { + $r = $prefix; + foreach ($xs as $x) $r .= (string) $x; + return $r; +} + +var_dump(concat::(":: ", 1, 2, 3)); + +try { + concat::(":: ", 1, [4, 5], 3); +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} + +// Object T over a variadic: a value of the wrong class fires with the bound +// class name, not the parameter letter. +class Animal {} +class Dog extends Animal {} +class Cat extends Animal {} + +function herd(T ...$xs): int { + return count($xs); +} + +var_dump(herd::(new Dog, new Dog, new Dog)); + +try { + herd::(new Dog, new Cat, new Dog); +} catch (TypeError $e) { + echo "4: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(6) +1: sum(): Argument #2 ($xs) must be of type int, string given +2: sum(): Argument #3 ($xs) must be of type int, string given +string(6) ":: 123" +3: concat(): Argument #3 ($xs) must be of type int, array given +int(3) +4: herd(): Argument #2 ($xs) must be of type Dog, Cat given diff --git a/Zend/tests/generics/reification/variance_reified.phpt b/Zend/tests/generics/reification/variance_reified.phpt new file mode 100644 index 000000000000..4f068d72d05b --- /dev/null +++ b/Zend/tests/generics/reification/variance_reified.phpt @@ -0,0 +1,54 @@ +--TEST-- +Reification: positive inheritance-variance cases — child signatures substituted against the reified T compile correctly +--FILE-- + { + public function get(): T { throw new Exception('abstract'); } + public function set(T $x): void {} +} + +// Child matches the substituted parent signature exactly. +class SubInt extends Base { + public function get(): int { return 42; } + public function set(int $x): void {} +} + +var_dump((new SubInt)->get()); +(new SubInt)->set(99); +echo "SubInt ok\n"; + +// Param contravariance: child accepts a WIDER param (mixed accepts int) +// than the parent's substituted int. This is allowed. +class SubParamWide extends Base { + public function set(mixed $x): void {} +} +(new SubParamWide)->set(7); +(new SubParamWide)->set("hello"); +echo "SubParamWide ok\n"; + +// Return-type covariance with class hierarchy: parent returns T = Animal, +// child returns Dog (a subclass of Animal). Allowed. +class Animal {} +class Dog extends Animal {} + +class AnimalSrc { + public function get(): T { throw new Exception('abstract'); } +} + +class DogSrc extends AnimalSrc { + public function get(): Dog { return new Dog; } +} +var_dump(get_class((new DogSrc)->get())); +?> +--EXPECT-- +int(42) +SubInt ok +SubParamWide ok +string(3) "Dog" diff --git a/Zend/tests/generics/reification/variance_reified_violation_param.phpt b/Zend/tests/generics/reification/variance_reified_violation_param.phpt new file mode 100644 index 000000000000..42e42625e700 --- /dev/null +++ b/Zend/tests/generics/reification/variance_reified_violation_param.phpt @@ -0,0 +1,15 @@ +--TEST-- +Reification: child narrowing a generic parent's reified parameter type (int → string) is rejected by variance (contravariance) +--FILE-- + { + public function set(T $x): void {} +} + +// Child accepts NARROWER (string) than parent's reified int — not contravariant. +class SubParam extends Base { + public function set(string $x): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of SubParam::set(string $x): void must be compatible with Base::set(int $x): void in %s on line %d diff --git a/Zend/tests/generics/reification/variance_reified_violation_return.phpt b/Zend/tests/generics/reification/variance_reified_violation_return.phpt new file mode 100644 index 000000000000..2997cdcaa5e9 --- /dev/null +++ b/Zend/tests/generics/reification/variance_reified_violation_return.phpt @@ -0,0 +1,15 @@ +--TEST-- +Reification: child overriding a generic parent method with a non-compatible return type fires the variance check against the reified T +--FILE-- + { + public function get(): T { throw new Exception('abstract'); } +} + +// Child says get(): string but parent's reified return is int. +class SubIntBroken extends Base { + public function get(): string { return "x"; } +} +?> +--EXPECTF-- +Fatal error: Declaration of SubIntBroken::get(): string must be compatible with Base::get(): int in %s on line %d diff --git a/Zend/tests/generics/reification/variance_reified_violation_widening.phpt b/Zend/tests/generics/reification/variance_reified_violation_widening.phpt new file mode 100644 index 000000000000..e48d75c1b578 --- /dev/null +++ b/Zend/tests/generics/reification/variance_reified_violation_widening.phpt @@ -0,0 +1,15 @@ +--TEST-- +Reification: child widening a generic parent's reified return type (int → mixed) is rejected by variance +--FILE-- + { + public function get(): T { throw new Exception('abstract'); } +} + +// Child can't widen the parent's reified int return to mixed. +class SubIntWiden extends Base { + public function get(): mixed { return 42; } +} +?> +--EXPECTF-- +Fatal error: Declaration of SubIntWiden::get(): mixed must be compatible with Base::get(): int in %s on line %d diff --git a/Zend/tests/generics/runtime/monomorph_unserialize_fresh_process.phpt b/Zend/tests/generics/runtime/monomorph_unserialize_fresh_process.phpt new file mode 100644 index 000000000000..56eee80829e8 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_unserialize_fresh_process.phpt @@ -0,0 +1,63 @@ +--TEST-- +Monomorph unserialize: a fresh process can unserialize a canonical generic name even when no instance has been constructed yet +--FILE-- +); unserialize() in a fresh process must +// be able to materialize that monomorph just from the name, without the +// caller priming it by constructing an instance first. + +class Box { + public function __construct(public T $value) {} +} + +// Produce the canonical payload from a primed process. +$payload = serialize(new Box::(42)); + +$tmp = tempnam(sys_get_temp_dir(), 'monomorph_unserialize_'); +file_put_contents($tmp, $payload); + +// Run the unserialize side in a fresh PHP process. The child declares the +// generic template but never constructs an instance, so the monomorph +// Box only exists as a name in the serialized payload. +$script = <<<'PHP' + { + public function __construct(public T $value) {} +} +$s = file_get_contents($argv[1]); +$b = unserialize($s); +$out = "###RESULT###\n"; +$out .= var_export($b instanceof Box, true) . "\n"; +$out .= var_export($b::class, true) . "\n"; +$out .= var_export($b->value, true) . "\n"; +file_put_contents($argv[2], $out); +PHP; + +$scriptFile = tempnam(sys_get_temp_dir(), 'monomorph_unserialize_script_'); +file_put_contents($scriptFile, $script); + +$resultFile = tempnam(sys_get_temp_dir(), 'monomorph_unserialize_result_'); + +$cmd = sprintf( + '%s -n %s %s %s >/dev/null 2>&1', + escapeshellarg(PHP_BINARY), + escapeshellarg($scriptFile), + escapeshellarg($tmp), + escapeshellarg($resultFile), +); +shell_exec($cmd); + +echo $payload, "\n"; +echo file_get_contents($resultFile); + +@unlink($tmp); +@unlink($scriptFile); +@unlink($resultFile); +?> +--EXPECT-- +O:8:"Box":1:{s:5:"value";i:42;} +###RESULT### +true +'Box' +42 diff --git a/Zend/tests/generics/scoping/trait_use_clause.phpt b/Zend/tests/generics/scoping/trait_use_clause.phpt index 7f3ff0b30690..9c50c7a98a53 100644 --- a/Zend/tests/generics/scoping/trait_use_clause.phpt +++ b/Zend/tests/generics/scoping/trait_use_clause.phpt @@ -1,5 +1,5 @@ --TEST-- -Scoping: T is visible in trait use clause (no compile error) +Scoping: a trait `use Holder` flows the using class's T-bound into the trait method's signature --FILE-- { @@ -8,10 +8,25 @@ trait Holder { class Box { use Holder; } + +// The trait method's X gets substituted with Box's T, which is bounded to +// object. The erased signature reflects that bound — not the trait's +// original unbounded "mixed". echo (new ReflectionClass('Box'))->getMethod('tag')->getReturnType()->getName(), "\n"; + +// On a bare (unmonomorphed) Box, the runtime check enforces the bound. $b = new Box; echo get_class($b), "\n"; +var_dump($b->tag(new stdClass) instanceof stdClass); + +try { + $b->tag(42); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} ?> ---EXPECT-- -mixed +--EXPECTF-- +object Box +bool(true) +rejected: Box::tag(): Argument #1 ($x) must be of type object, int given%S diff --git a/Zend/tests/generics/syntax/args_dnf.phpt b/Zend/tests/generics/syntax/args_dnf.phpt index 61829c694c59..e0903258189e 100644 --- a/Zend/tests/generics/syntax/args_dnf.phpt +++ b/Zend/tests/generics/syntax/args_dnf.phpt @@ -5,7 +5,7 @@ Generic syntax: type argument is DNF interface A {} interface B {} class C {} -class D {} +class D {} function f(D<(A&B)|C> $x): void {} $pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); $arg = $pt->getGenericArguments()[0]; diff --git a/Zend/tests/generics/syntax/args_intersection.phpt b/Zend/tests/generics/syntax/args_intersection.phpt index 5cd01bda2a66..9842148e216e 100644 --- a/Zend/tests/generics/syntax/args_intersection.phpt +++ b/Zend/tests/generics/syntax/args_intersection.phpt @@ -4,7 +4,7 @@ Generic syntax: type argument is an intersection {} function f(C $x): void {} $pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); $arg = $pt->getGenericArguments()[0]; diff --git a/Zend/tests/generics/syntax/args_union.phpt b/Zend/tests/generics/syntax/args_union.phpt index 206b88266774..1e424f8bf363 100644 --- a/Zend/tests/generics/syntax/args_union.phpt +++ b/Zend/tests/generics/syntax/args_union.phpt @@ -2,7 +2,7 @@ Generic syntax: type argument is a union --FILE-- {} function f(Container $x): void {} $pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); $arg = $pt->getGenericArguments()[0]; diff --git a/Zend/tests/generics/syntax/bound_and_default_with_args.phpt b/Zend/tests/generics/syntax/bound_and_default_with_args.phpt index 620484d0bf05..832cdada25e4 100644 --- a/Zend/tests/generics/syntax/bound_and_default_with_args.phpt +++ b/Zend/tests/generics/syntax/bound_and_default_with_args.phpt @@ -4,8 +4,10 @@ Generic syntax: bound and default both carry type arguments, closing >> {} -// class A has a generic parameter T with bound B and default B -class A=B> {} +// class A has a generic parameter T with bound B and default B. +// Same args on both so the default satisfies the bound under invariant T — +// the test exercises the parser's `>>` splitting in bound + default position. +class A=B> {} $p = (new ReflectionClass('A'))->getGenericParameters()[0]; @@ -16,5 +18,5 @@ $d = $p->getDefault(); echo "default: ", $d->getName(), "<", $d->getGenericArguments()[0]->getName(), ">\n"; ?> --EXPECT-- -bound: B +bound: B default: B diff --git a/Zend/tests/generics/syntax/catch_with_args.phpt b/Zend/tests/generics/syntax/catch_with_args.phpt index d97bf24cd8b9..bef722884955 100644 --- a/Zend/tests/generics/syntax/catch_with_args.phpt +++ b/Zend/tests/generics/syntax/catch_with_args.phpt @@ -2,19 +2,16 @@ Generic syntax: catch with type arguments compares against the monomorph canonical name --FILE-- does not exist as a -// class. catch (MyErr) therefore never matches; the original exception -// propagates to the outer catch. +class MyErr extends Exception {} +// Throwing MyErr matches a `catch (MyErr)` block; a `catch (MyErr)` +// would not, because the two are distinct monomorphs. try { - try { - throw new MyErr('boom'); - } catch (MyErr $e) { - echo "inner caught: ", $e->getMessage(), "\n"; - } -} catch (MyErr $e) { - echo "outer caught: ", $e->getMessage(), "\n"; + throw new MyErr::('boom'); +} catch (MyErr $e) { + echo "wrong-mono caught: ", $e->getMessage(), "\n"; +} catch (MyErr $e) { + echo "right-mono caught: ", $e->getMessage(), "\n"; } ?> --EXPECT-- -outer caught: boom +right-mono caught: boom diff --git a/Zend/tests/generics/syntax/default_with_nested_args.phpt b/Zend/tests/generics/syntax/default_with_nested_args.phpt index 8a2af325ff86..ef09d58a7b47 100644 --- a/Zend/tests/generics/syntax/default_with_nested_args.phpt +++ b/Zend/tests/generics/syntax/default_with_nested_args.phpt @@ -2,10 +2,10 @@ Generic syntax: >>= splitting in default position --FILE-- {} class C> {} $p = (new ReflectionClass('C'))->getGenericParameters()[0]; echo $p, "\n"; ?> --EXPECT-- -T = MapT +T = MapT diff --git a/Zend/tests/generics/syntax/instanceof_with_args.phpt b/Zend/tests/generics/syntax/instanceof_with_args.phpt index e98242f11ac5..1722eff49681 100644 --- a/Zend/tests/generics/syntax/instanceof_with_args.phpt +++ b/Zend/tests/generics/syntax/instanceof_with_args.phpt @@ -2,16 +2,16 @@ Generic syntax: instanceof with type arguments resolves to the monomorph canonical name --FILE-- and C do not exist -// as classes, so instanceof returns false. +class C {} +$c = new C::(); +var_dump($c instanceof C); var_dump($c instanceof C); -var_dump($c instanceof C); +var_dump($c instanceof C); $x = new stdClass; var_dump($x instanceof C); ?> --EXPECT-- -bool(false) +bool(true) +bool(true) bool(false) bool(false) diff --git a/Zend/tests/generics/syntax/named_type_args.phpt b/Zend/tests/generics/syntax/named_type_args.phpt index 91e98d4c6273..7e4151ccd8e2 100644 --- a/Zend/tests/generics/syntax/named_type_args.phpt +++ b/Zend/tests/generics/syntax/named_type_args.phpt @@ -2,15 +2,17 @@ Generic syntax: type arguments on a named type at use site --FILE-- {} function f(Container $x): Container { return $x; } $r = new ReflectionFunction('f'); $pt = $r->getParameters()[0]->getType(); +// With reified params, the type's name is the canonical monomorph, not the +// bare base. Generic args are still queryable via getGenericArguments(). echo $pt->getName(), "\n"; var_dump($pt->hasGenericArguments()); foreach ($pt->getGenericArguments() as $a) echo $a->getName(), "\n"; ?> --EXPECT-- -Container +Container bool(true) int diff --git a/Zend/tests/generics/syntax/nested_three_levels.phpt b/Zend/tests/generics/syntax/nested_three_levels.phpt index 9095735c9ae0..f68dae761c32 100644 --- a/Zend/tests/generics/syntax/nested_three_levels.phpt +++ b/Zend/tests/generics/syntax/nested_three_levels.phpt @@ -2,10 +2,10 @@ Generic syntax: three levels of nesting (>>> splitting) --FILE-- >> { return new A; } +class C {} +class B {} +class A {} +function f(): A>> { return new A::>>(); } $rt = (new ReflectionFunction('f'))->getReturnType(); echo $rt->getName(), "\n"; $arg1 = $rt->getGenericArguments()[0]; @@ -15,7 +15,7 @@ echo $arg2->getName(), "\n"; echo $arg2->getGenericArguments()[0]->getName(), "\n"; ?> --EXPECT-- -A +A>> B C int diff --git a/Zend/tests/generics/syntax/nested_two_levels.phpt b/Zend/tests/generics/syntax/nested_two_levels.phpt index f5d3dd38e86f..d5c43c30a455 100644 --- a/Zend/tests/generics/syntax/nested_two_levels.phpt +++ b/Zend/tests/generics/syntax/nested_two_levels.phpt @@ -2,9 +2,9 @@ Generic syntax: two levels of nesting (>> splitting) --FILE-- > { return new Outer; } +class Inner {} +class Outer {} +function f(): Outer> { return new Outer::>(); } $rt = (new ReflectionFunction('f'))->getReturnType(); echo $rt->getName(), "\n"; $arg = $rt->getGenericArguments()[0]; @@ -12,6 +12,6 @@ echo $arg->getName(), "\n"; echo $arg->getGenericArguments()[0]->getName(), "\n"; ?> --EXPECT-- -Outer +Outer> Inner int diff --git a/Zend/tests/generics/syntax/trailing_comma_args.phpt b/Zend/tests/generics/syntax/trailing_comma_args.phpt index d7de432dea15..1b8c2c93563f 100644 --- a/Zend/tests/generics/syntax/trailing_comma_args.phpt +++ b/Zend/tests/generics/syntax/trailing_comma_args.phpt @@ -2,7 +2,7 @@ Generic syntax: trailing comma in type argument list --FILE-- {} function f(C $x): void {} $pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); echo count($pt->getGenericArguments()), "\n"; diff --git a/Zend/tests/generics/turbofish/arity/new_arity.phpt b/Zend/tests/generics/turbofish/arity/new_arity.phpt index 4f69cf3663c4..f7ff94b67a3e 100644 --- a/Zend/tests/generics/turbofish/arity/new_arity.phpt +++ b/Zend/tests/generics/turbofish/arity/new_arity.phpt @@ -6,14 +6,10 @@ class Box { public function __construct(public int $v) {} } class Pair {} -class Plain {} try { new Box::(1); } catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } -try { new Plain::(); } -catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } - // Pair: K required, V has default - so 1 or 2 args is OK, 0 or 3 is not. // With monomorphization, the missing trailing arg is filled from the default, // so the canonical name reflects the substituted Pair. @@ -25,6 +21,5 @@ catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } ?> --EXPECT-- Too many generic type arguments to new Box, 2 passed and exactly 1 expected -Too many generic type arguments to new Plain, 1 passed and exactly 0 expected Pair Too many generic type arguments to new Pair, 3 passed and at most 2 expected diff --git a/Zend/tests/generics/turbofish/instanceof_with_args.phpt b/Zend/tests/generics/turbofish/instanceof_with_args.phpt index 2e636db546ab..459885fcdfdf 100644 --- a/Zend/tests/generics/turbofish/instanceof_with_args.phpt +++ b/Zend/tests/generics/turbofish/instanceof_with_args.phpt @@ -3,14 +3,9 @@ Turbofish: instanceof with type arguments compares against the monomorph canonic --FILE-- {} -class C {} class Animal {} class Dog {} -$c = new C; -var_dump($c instanceof C); // true -var_dump($c instanceof C); // false: C is non-generic; C does not exist - $b = new B::(); var_dump($b instanceof B); // true: every mono extends the bare class var_dump($b instanceof B); // true: same canonical mono @@ -18,7 +13,5 @@ var_dump($b instanceof B); // false: distinct mono ?> --EXPECT-- bool(true) -bool(false) -bool(true) bool(true) bool(false) diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index 840d2dbe32e1..c0ceee2ea756 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -27,13 +27,8 @@ #include "zend_globals.h" #include "zend_closures_arginfo.h" -typedef struct _zend_closure { - zend_object std; - zend_function func; - zval this_ptr; - zend_class_entry *called_scope; - zif_handler orig_internal_handler; -} zend_closure; +/* zend_closure struct is defined in zend_closures.h so VM hot paths can + * inline the captured_type_args propagation in zend_vm_init_call_frame. */ /* non-static since it needs to be referenced */ ZEND_API zend_class_entry *zend_ce_closure; @@ -174,11 +169,17 @@ ZEND_METHOD(Closure, call) zend_function *my_function; fake_closure = emalloc(sizeof(zend_closure)); - memset(&fake_closure->std, 0, sizeof(fake_closure->std)); + memset(fake_closure, 0, sizeof(zend_closure)); fake_closure->std.gc.refcount = 1; fake_closure->std.gc.u.type_info = GC_NULL; ZVAL_UNDEF(&fake_closure->this_ptr); fake_closure->called_scope = NULL; + /* The fake closure runs the source closure's body; that body still + * resolves outer T-refs against the source's captured T-table. Borrow + * the pointer (no clone) — the source closure outlives the fake, + * which is efree'd at the end of this method without running + * free_storage, so no double-destroy. */ + fake_closure->captured_type_args = closure->captured_type_args; my_function = &fake_closure->func; if (ZEND_USER_CODE(closure->func.type)) { memcpy(my_function, &closure->func, sizeof(zend_op_array)); @@ -252,6 +253,14 @@ static void do_closure_bind(zval *return_value, zval *zclosure, zval *newthis, z } zend_create_closure(return_value, &closure->func, ce, called_scope, newthis); + + /* Re-binding produces a new closure that runs the same body; the body + * still references the outer T-bindings captured at lambda declaration + * time, so carry the T-table over. Without this Closure::bind and + * Closure::bindTo silently erase the reified bindings. */ + if (closure->captured_type_args) { + zend_closure_capture_type_args(return_value, closure->captured_type_args); + } } /* {{{ Create a closure from another one and bind to another object and scope */ @@ -567,6 +576,12 @@ static void zend_closure_free_storage(zend_object *object) /* {{{ */ if (Z_TYPE(closure->this_ptr) != IS_UNDEF) { zval_ptr_dtor(&closure->this_ptr); } + + if (closure->captured_type_args) { + closure->captured_type_args->persisted = false; + zend_type_arg_table_destroy(closure->captured_type_args); + closure->captured_type_args = NULL; + } } /* }}} */ @@ -590,6 +605,13 @@ static zend_object *zend_closure_clone(zend_object *zobject) /* {{{ */ zend_create_closure(&result, &closure->func, closure->func.common.scope, closure->called_scope, &closure->this_ptr); + + /* Carry over the source closure's captured T-table; the clone has the + * same body and resolves the same outer T-refs. */ + if (closure->captured_type_args) { + zend_closure_capture_type_args(&result, closure->captured_type_args); + } + return Z_OBJ(result); } /* }}} */ @@ -857,6 +879,19 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en } } } + +ZEND_API void zend_closure_capture_type_args(zval *closure_zv, zend_type_arg_table *src) +{ + zend_closure *closure = (zend_closure *) Z_OBJ_P(closure_zv); + if (closure->captured_type_args) { + closure->captured_type_args->persisted = false; + zend_type_arg_table_destroy(closure->captured_type_args); + closure->captured_type_args = NULL; + } + if (src) { + closure->captured_type_args = zend_type_arg_table_capture_clone(src); + } +} /* }}} */ ZEND_API void zend_create_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr) diff --git a/Zend/zend_closures.h b/Zend/zend_closures.h index 2ff4934f2a3a..b7ca1edd65f5 100644 --- a/Zend/zend_closures.h +++ b/Zend/zend_closures.h @@ -23,6 +23,19 @@ BEGIN_EXTERN_C() +typedef struct _zend_closure { + zend_object std; + zend_function func; + zval this_ptr; + zend_class_entry *called_scope; + zif_handler orig_internal_handler; + /* Snapshot of the generic type-arg table from the frame that created + * the closure. The closure owns the allocation; it is marked persisted + * so call-frame teardown skips destroying it. Cleared on closure + * free_obj. NULL when the closure was created outside a generic frame. */ + struct _zend_type_arg_table *captured_type_args; +} zend_closure; + /* This macro depends on zend_closure structure layout */ #define ZEND_CLOSURE_OBJECT(op_array) \ ((zend_object*)((char*)(op_array) - sizeof(zend_object))) @@ -36,6 +49,7 @@ extern ZEND_API zend_class_entry *zend_ce_closure; ZEND_API void zend_create_closure(zval *res, zend_function *op_array, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr); ZEND_API void zend_create_fake_closure(zval *res, zend_function *op_array, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr); +ZEND_API void zend_closure_capture_type_args(zval *closure_zv, struct _zend_type_arg_table *src); ZEND_API zend_function *zend_get_closure_invoke_method(zend_object *obj); ZEND_API const zend_function *zend_get_closure_method_def(zend_object *obj); ZEND_API zval* zend_get_closure_this_ptr(zval *obj); diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 165cfb6dbf8c..6ae5745e110d 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -610,7 +610,8 @@ static void zend_check_generic_argument_bounds( const zend_type *args_box, uint32_t arity, const char *callee_kind, - const char *callee_qualified_name) + const zend_function *callee_fbc, + const zend_class_entry *callee_ce) { if (!params || !args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { return; @@ -630,12 +631,24 @@ static void zend_check_generic_argument_bounds( ? params->parameters[i].bound_pre_erasure : bound; zend_string *bound_str = zend_type_to_string(display); + /* Build the callee's qualified display name only on the error + * path — the success path runs on every generic call. */ + zend_string *qname; + if (callee_fbc) { + qname = zend_strpprintf(0, "%s%s%s()", + callee_fbc->common.scope ? ZSTR_VAL(callee_fbc->common.scope->name) : "", + callee_fbc->common.scope ? "::" : "", + callee_fbc->common.function_name ? ZSTR_VAL(callee_fbc->common.function_name) : "{closure}"); + } else { + qname = zend_string_copy(callee_ce->name); + } zend_throw_error(zend_ce_type_error, "Type argument %u to %s %s does not satisfy the bound %s on parameter %s, %s given", - i + 1, callee_kind, callee_qualified_name, + i + 1, callee_kind, ZSTR_VAL(qname), ZSTR_VAL(bound_str), params->parameters[i].name ? ZSTR_VAL(params->parameters[i].name) : "?", ZSTR_VAL(arg_str)); + zend_string_release(qname); zend_string_release(arg_str); zend_string_release(bound_str); return; @@ -924,7 +937,13 @@ static bool zend_check_pre_erasure_type_value( * to the erased bound check at RECV. */ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box) { - if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = NULL; + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + } + /* When neither args_box nor a captured frame T-table is available there + * is nothing to verify against. */ + if (!nwa && !call->type_args) { return true; } const zend_function *fbc = call->func; @@ -933,7 +952,6 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ || !fbc->op_array.generic_types->parameters) { return true; } - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); const HashTable *pre = fbc->op_array.generic_types->parameters; uint32_t num_args = ZEND_CALL_NUM_ARGS(call); zend_ulong arg_idx; @@ -943,16 +961,21 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; - if (ref->index >= nwa->count) continue; - zend_type substituted = nwa->args[ref->index]; - if (!ZEND_TYPE_IS_SET(substituted)) continue; - zval *arg = ZEND_CALL_ARG(call, (uint32_t) arg_idx + 1); - zval *target = arg; - const zend_reference *zref = NULL; - if (Z_ISREF_P(target)) { - zref = Z_REF_P(target); - target = Z_REFVAL_P(target); + zend_type substituted; + if (nwa) { + if (ref->index >= nwa->count) continue; + substituted = nwa->args[ref->index]; + } else { + /* No turbofish args_box for this call — resolve directly from the + * captured T-table (e.g., a closure with no turbofish at its own + * call site, whose body references the outer frame's T). */ + if (ref->index >= call->type_args->count) continue; + const zend_type *resolved = zend_type_arg_entry_type( + &call->type_args->entries[ref->index]); + if (!resolved) continue; + substituted = *resolved; } + if (!ZEND_TYPE_IS_SET(substituted)) continue; /* If the turbofish arg is itself a forwarded TYPE_PARAMETER (e.g. * `id::($x)` inside `nested`), the caller's binding has @@ -974,34 +997,125 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ substituted = *resolved; } - /* Pre-erasure shapes don't match the canonical PHP type layout - * (scalars sit as CODE-only list members instead of bits on the - * outer mask), so walk them directly. */ bool strict = EG(current_execute_data) && EG(current_execute_data)->func && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); - bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); - if (!ok) { - zend_string *expected = zend_type_to_string(substituted); - const zend_arg_info *ai = (arg_idx < fbc->common.num_args) - ? &fbc->common.arg_info[arg_idx] : NULL; - zend_throw_error(zend_ce_type_error, - "%s%s%s(): Argument #%u%s%s%s must be of type %s, %s given", - fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", - fbc->common.scope ? "::" : "", - fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", - (uint32_t) arg_idx + 1, - (ai && ai->name) ? " ($" : "", - (ai && ai->name) ? ZSTR_VAL(ai->name) : "", - (ai && ai->name) ? ")" : "", - ZSTR_VAL(expected), zend_zval_value_name(target)); - zend_string_release(expected); - return false; + + /* When the pre-erasure parameter slot is variadic (T ...$xs), the + * single key in `pre` covers every value supplied at runtime. Sweep + * from arg_idx through num_args-1 so each variadic element gets the + * same reified T-check, not just the first. */ + bool is_variadic_slot = (fbc->common.fn_flags & ZEND_ACC_VARIADIC) + && arg_idx == fbc->common.num_args; + uint32_t sweep_end = is_variadic_slot ? num_args : (uint32_t) arg_idx + 1; + + for (uint32_t aidx = (uint32_t) arg_idx; aidx < sweep_end; aidx++) { + zval *arg = ZEND_CALL_ARG(call, aidx + 1); + zval *target = arg; + const zend_reference *zref = NULL; + if (Z_ISREF_P(target)) { + zref = Z_REF_P(target); + target = Z_REFVAL_P(target); + } + + /* Pre-erasure shapes don't match the canonical PHP type layout + * (scalars sit as CODE-only list members instead of bits on the + * outer mask), so walk them directly. */ + bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); + if (!ok) { + zend_string *expected = zend_type_to_string(substituted); + const zend_arg_info *ai; + if (is_variadic_slot) { + ai = &fbc->common.arg_info[fbc->common.num_args]; + } else { + ai = (aidx < fbc->common.num_args) + ? &fbc->common.arg_info[aidx] : NULL; + } + zend_throw_error(zend_ce_type_error, + "%s%s%s(): Argument #%u%s%s%s must be of type %s, %s given", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + aidx + 1, + (ai && ai->name) ? " ($" : "", + (ai && ai->name) ? ZSTR_VAL(ai->name) : "", + (ai && ai->name) ? ")" : "", + ZSTR_VAL(expected), zend_zval_value_name(target)); + zend_string_release(expected); + return false; + } } } ZEND_HASH_FOREACH_END(); return true; } +/* Reified counterpart to zend_verify_return_error's erased check. Consults + * op_array->generic_types->return_type (pre-erasure) and, if it's a direct + * T-ref, substitutes via call->type_args and verifies the value. Only bare + * T at the top level is handled; composite shapes (array, Box) still + * fall through to the existing erased-bound check. Returns true when no + * reified check applies *or* the check passes; false (with EG(exception) + * set) when the value violates the reified type. */ +ZEND_API bool zend_verify_generic_return_type(zend_execute_data *call, zval *retval_ptr) +{ + if (!call->type_args) { + return true; + } + const zend_function *fbc = call->func; + if (!ZEND_USER_CODE(fbc->common.type) + || !fbc->op_array.generic_types + || !fbc->op_array.generic_types->return_type) { + return true; + } + const zend_type *pe_type = fbc->op_array.generic_types->return_type; + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type)) { + return true; + } + const zend_type_parameter_ref *param_ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type); + if (param_ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + return true; + } + if (param_ref->index >= call->type_args->count) { + return true; + } + const zend_type *resolved = zend_type_arg_entry_type( + &call->type_args->entries[param_ref->index]); + if (!resolved || !ZEND_TYPE_IS_SET(*resolved)) { + return true; + } + zend_type substituted = *resolved; + + zval *target = retval_ptr; + const zend_reference *zref = NULL; + if (Z_ISREF_P(target)) { + zref = Z_REF_P(target); + target = Z_REFVAL_P(target); + } + + /* `?T` carries the nullable bit on the outer T-ref; preserve it on the + * substituted type so a returned NULL still passes when declared `?T`. */ + if (ZEND_TYPE_FULL_MASK(*pe_type) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(substituted) |= _ZEND_TYPE_NULLABLE_BIT; + } + + bool strict = EG(current_execute_data) + && EG(current_execute_data)->func + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); + bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); + if (!ok) { + zend_string *expected = zend_type_to_string(substituted); + zend_throw_error(zend_ce_type_error, + "%s%s%s(): Return value must be of type %s, %s returned", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + ZSTR_VAL(expected), zend_zval_value_name(target)); + zend_string_release(expected); + return false; + } + return true; +} + ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box) { const zend_generic_parameter_list *params = NULL; @@ -1048,12 +1162,7 @@ ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32 } if (args_box) { - zend_string *qname = zend_strpprintf(0, "%s%s%s()", - fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", - fbc->common.scope ? "::" : "", - fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}"); - zend_check_generic_argument_bounds(params, args_box, arity, "call", ZSTR_VAL(qname)); - zend_string_release(qname); + zend_check_generic_argument_bounds(params, args_box, arity, "call", fbc, NULL); } } @@ -1078,7 +1187,7 @@ ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint3 } if (args_box) { - zend_check_generic_argument_bounds(ce->generic_parameters, args_box, arity, "new", ZSTR_VAL(ce->name)); + zend_check_generic_argument_bounds(ce->generic_parameters, args_box, arity, "new", NULL, ce); } } @@ -1116,11 +1225,17 @@ static bool zend_static_check_generic_call_bounds( uint32_t check_count = arity < args->count ? arity : args->count; if (check_count > params->count) check_count = params->count; for (uint32_t i = 0; i < check_count; i++) { + zend_type bound = params->parameters[i].bound; + bool has_bound = ZEND_TYPE_IS_SET(bound); if (zend_type_contains_type_parameter(args->args[i])) { + /* Unbounded parameter accepts any T-ref trivially; we can elide + * the runtime arity+bound check (INSTALL instead of VERIFY). + * A T-ref against a bounded parameter still needs runtime + * resolution to compare against the bound. */ + if (!has_bound) continue; return false; } - zend_type bound = params->parameters[i].bound; - if (!ZEND_TYPE_IS_SET(bound)) continue; + if (!has_bound) continue; zend_inheritance_status status = zend_check_generic_arg_satisfies_bound(NULL, args->args[i], NULL, bound); if (status != INHERITANCE_SUCCESS) { @@ -1383,6 +1498,23 @@ static void zend_reject_typearg_on_iterable(zend_ast *ast) } } +/* When a named class type carries type arguments (Foo), the named class + * must actually be generic. If the class is already in CG(class_table) and + * has no generic_parameters, raise a hard compile-time error. Classes not yet + * seen by the compiler fall through; the runtime monomorph lookup raises the + * same error in that case. */ +static void zend_validate_class_accepts_type_args(zend_string *resolved_name) +{ + zend_string *lc = zend_string_tolower(resolved_name); + zend_class_entry *ce = zend_hash_find_ptr(CG(class_table), lc); + zend_string_release(lc); + if (ce && !ce->generic_parameters) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type arguments are not allowed on non-generic class %s", + ZSTR_VAL(ce->name)); + } +} + /* Build a pre-erasure zend_type from a type-expression AST. The returned type * may carry type-parameter references (TYPE_PARAMETER bit) or named-with-args * payloads (NAMED_WITH_ARGS bit). Used only for the side table; the runtime @@ -1421,7 +1553,12 @@ static zend_type zend_compile_pre_erasure_typename(zend_ast *ast) if (zend_get_class_fetch_type(raw) != ZEND_FETCH_CLASS_DEFAULT) { payload->name = zend_string_copy(raw); } else { + bool is_type_param = ((name_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) + && zend_generic_lookup(raw) != NULL; payload->name = zend_resolve_class_name(raw, name_ast->attr); + if (!is_type_param) { + zend_validate_class_accepts_type_args(payload->name); + } } payload->name_attr = name_ast->attr; @@ -4080,20 +4217,28 @@ static void zend_emit_return_type_check( } } + /* Generic functions: if the return is a T-ref, the runtime opcode + * is the only place the reified binding gets checked. Detect that + * here so the two elision paths below don't drop the opcode. */ + zend_op_array *active = CG(active_op_array); + const zend_type *pre_return = + active->generic_types ? active->generic_types->return_type : NULL; + bool return_is_generic = pre_return + && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_return); + if (expr && ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY) { /* Mixed normally needs no run-time check, but if the return is a * generic parameter that erased to mixed, a child substituting T to * a concrete type relies on this opcode being present in the shared * body so its substituted arg_info gets checked. */ - zend_op_array *active = CG(active_op_array); - const zend_type *pre_return = - active->generic_types ? active->generic_types->return_type : NULL; - if (!pre_return || !ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_return)) { + if (!return_is_generic) { return; } } - if (expr && expr->op_type == IS_CONST && ZEND_TYPE_CONTAINS_CODE(type, Z_TYPE(expr->u.constant))) { + if (!return_is_generic + && expr && expr->op_type == IS_CONST + && ZEND_TYPE_CONTAINS_CODE(type, Z_TYPE(expr->u.constant))) { /* we don't need run-time check */ return; } @@ -7215,78 +7360,141 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ } } - /* Naked `new GenericClass()` (no turbofish): build the canonical monomorph - * name and rewrite op1 to it so the lookup hook synthesizes the monomorph. + /* Reject turbofish on a known non-generic class at compile time so the + * error matches the type-position behavior (rather than waiting for + * VERIFY_GENERIC_ARGUMENTS to throw ArgumentCountError at runtime). */ + if (ce && !ce->generic_parameters && turbofish_ast) { + zend_ast_list *tf_list = zend_ast_get_list(turbofish_ast); + if (tf_list->children > 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type arguments are not allowed on non-generic class %s", + ZSTR_VAL(ce->name)); + } + } + + /* Compile-time monomorph synthesis for `new GenericClass(...)`. Builds the + * canonical name, synthesizes the monomorph, and rewrites op1 to the + * canonical so ZEND_NEW creates the monomorph directly. When this fires + * we also drop the runtime ZEND_VERIFY_GENERIC_ARGUMENTS opcode (the + * arity + bound check is now redundant) and swap fbc to the monomorph's + * constructor so RECV checks against the substituted arg_info. * - * Handles three resolved-at-compile-time forms: - * - literal class name → use its declared defaults; error if any param - * has no default (the user has no way to bind T here) - * - `self` → use the active class's declared defaults; if any param has - * no default, leave the opcode alone and the runtime creates a bare - * instance (preserving the existing lexical-self semantic and letting - * code inside a generic class refer to itself without forcing a - * defaults declaration) - * - `parent` → use the args from the `extends Foo<...>` clause if any, - * otherwise the parent's defaults; same lenient fallback as `self` + * Three sources for the type args, in priority order: + * - concrete turbofish `new C::(...)` — synthesize when every arg + * is concrete (no T-refs) and bound-checks pass at compile time. + * - `new parent()` inside `class X extends Foo` — borrow the args + * from the extends clause. + * - declared parameter defaults (naked `new GenericClass()`). * * `static` and dynamic names are handled at runtime in ZEND_NEW. */ - if (ce && ce->generic_parameters && !turbofish_ast) { + bool ct_synthesized = false; + zend_class_entry *mono_ce = NULL; + if (ce && ce->generic_parameters) { uint32_t count = ce->generic_parameters->count; const zend_type *src_args = NULL; uint32_t src_arity = 0; - if (parent_extends_args) { + zend_type tf_args_buf[ZEND_GENERIC_MAX_PARAMS]; + uint32_t tf_args_built_arity = 0; + bool tf_concrete = false; + + if (turbofish_ast) { + zend_ast_list *tf_list = zend_ast_get_list(turbofish_ast); + if (tf_list->children > 0 && tf_list->children <= count) { + tf_concrete = true; + for (uint32_t i = 0; i < tf_list->children; i++) { + tf_args_buf[i] = zend_compile_pre_erasure_typename(tf_list->child[i]); + tf_args_built_arity = i + 1; + if (zend_type_contains_type_parameter(tf_args_buf[i])) { + tf_concrete = false; + } + } + if (tf_concrete) { + src_args = tf_args_buf; + src_arity = tf_list->children; + } + } + } else if (parent_extends_args) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*parent_extends_args); src_args = nwa->args; src_arity = nwa->count; } - zend_type chosen[ZEND_GENERIC_MAX_PARAMS]; - bool ok = true; - /* Strict (compile-error on missing defaults) when the call site refers - * to a generic class from outside that class. Lenient (fall through to - * the bare class) when it's a lexical self-reference — including both - * `new self()` and `new ThisClass()` inside the class's own body — - * since the lexical scope semantically has access to T even though we - * can't bind it at compile time. */ - bool is_lexical_self = ce == CG(active_class_entry); - for (uint32_t i = 0; i < count; i++) { - if (i < src_arity) { - chosen[i] = src_args[i]; - continue; - } - const zend_generic_parameter *p = &ce->generic_parameters->parameters[i]; - if (!ZEND_TYPE_IS_SET(p->default_type)) { - if (!is_lexical_self) { - zend_error_noreturn(E_COMPILE_ERROR, - "Cannot instantiate generic class %s without type arguments; " - "type parameter %s has no default", - ZSTR_VAL(ce->name), ZSTR_VAL(p->name)); + + bool attempt_synth = (src_args != NULL) || !turbofish_ast; + if (attempt_synth) { + zend_type chosen[ZEND_GENERIC_MAX_PARAMS]; + bool ok = true; + /* Strict (compile-error on missing defaults) when the naked call + * site refers to a generic class from outside that class. Lenient + * (fall through to the bare class / runtime VERIFY) when it's a + * lexical self-reference or when turbofish is present — the + * runtime can still throw a catchable ArgumentCountError. */ + bool is_lexical_self = ce == CG(active_class_entry); + for (uint32_t i = 0; i < count; i++) { + if (i < src_arity) { + chosen[i] = src_args[i]; + continue; + } + const zend_generic_parameter *p = &ce->generic_parameters->parameters[i]; + if (!ZEND_TYPE_IS_SET(p->default_type)) { + if (!is_lexical_self && !turbofish_ast) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot instantiate generic class %s without type arguments; " + "type parameter %s has no default", + ZSTR_VAL(ce->name), ZSTR_VAL(p->name)); + } + ok = false; + break; + } + chosen[i] = ZEND_TYPE_IS_SET(p->default_pre_erasure) + ? p->default_pre_erasure : p->default_type; + } + /* Concrete turbofish args must satisfy each parameter's bound at + * compile time before we elide the runtime check. UNRESOLVED (a + * referenced class not yet loaded) and ERROR both fall through to + * runtime VERIFY so the user gets a catchable TypeError. */ + if (ok && tf_concrete) { + for (uint32_t i = 0; i < src_arity; i++) { + zend_type bound = ce->generic_parameters->parameters[i].bound; + if (!ZEND_TYPE_IS_SET(bound)) continue; + if (zend_check_generic_arg_satisfies_bound(NULL, src_args[i], NULL, bound) + != INHERITANCE_SUCCESS) { + ok = false; + break; + } } - ok = false; - break; } - chosen[i] = ZEND_TYPE_IS_SET(p->default_pre_erasure) - ? p->default_pre_erasure : p->default_type; + if (ok) { + zend_string *canonical = + zend_generic_canonical_class_name(ce->name, chosen, count); + zend_try_compile_time_synthesize_monomorph(canonical); + zend_string *lc = zend_string_tolower(canonical); + mono_ce = zend_hash_find_ptr(CG(class_table), lc); + zend_string_release(lc); + opline->op1_type = IS_CONST; + opline->op1.constant = zend_add_class_name_literal(canonical); + opline->op2.num = zend_alloc_cache_slot(); + ct_synthesized = true; + } } - if (ok) { - zend_string *canonical = - zend_generic_canonical_class_name(ce->name, chosen, count); - zend_try_compile_time_synthesize_monomorph(canonical); - opline->op1_type = IS_CONST; - opline->op1.constant = zend_add_class_name_literal(canonical); - opline->op2.num = zend_alloc_cache_slot(); + + for (uint32_t i = 0; i < tf_args_built_arity; i++) { + zend_type_release(tf_args_buf[i], /* persistent */ false); } } const zend_function *fbc = NULL; - if (ce - && ce->default_object_handlers->get_constructor == zend_std_get_constructor - && ce->constructor - && is_func_accessible(ce->constructor)) { - fbc = ce->constructor; + zend_class_entry *ctor_ce = mono_ce ? mono_ce : ce; + if (ctor_ce + && ctor_ce->default_object_handlers->get_constructor == zend_std_get_constructor + && ctor_ce->constructor + && is_func_accessible(ctor_ce->constructor)) { + fbc = ctor_ce->constructor; } - zend_compile_call_common(&ctor_result, args_ast, fbc, ast->lineno, BP_VAR_R, turbofish_ast, ZEND_VERIFY_ARITY_KIND_NEW, result); + zend_compile_call_common(&ctor_result, args_ast, fbc, ast->lineno, BP_VAR_R, + ct_synthesized ? NULL : turbofish_ast, + ZEND_VERIFY_ARITY_KIND_NEW, result); zend_do_free(&ctor_result); } /* }}} */ @@ -9007,6 +9215,38 @@ static zend_type zend_compile_single_typename(zend_ast *ast) ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE)); zend_reject_typearg_on_iterable(ast); if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + /* For concrete args (no T-refs), rewrite the named type as the + * canonical monomorph class — so the runtime arg_info-driven RECV + * check enforces `Box` identity, not just bare `Box`. T-ref + * args (`Box`) can't be resolved here and fall back to the + * erased base; the pre-erasure side table still carries the shape + * for reflection and inference. `self`/`parent`/`static` keep + * their lexical-scope meaning and also fall through. + * + * Builtin pseudo-types (`int`, `string`, ...) cannot carry type + * arguments — those are caught by the validation in + * zend_compile_pre_erasure_typename / the non-generic-class check. */ + zend_ast *name_ast = ast->child[0]; + bool is_relative_scope = false; + if (name_ast->kind == ZEND_AST_TYPE) { + is_relative_scope = true; + } else if (name_ast->kind == ZEND_AST_ZVAL) { + const zval *zv = zend_ast_get_zval(name_ast); + if (Z_TYPE_P(zv) == IS_STRING + && zend_get_class_fetch_type(Z_STR_P(zv)) != ZEND_FETCH_CLASS_DEFAULT) { + is_relative_scope = true; + } + } + if (!is_relative_scope) { + zend_type pre_erasure = zend_compile_pre_erasure_typename(ast); + if (!zend_type_contains_type_parameter(pre_erasure)) { + zend_string *canonical = zend_type_to_canonical_string(pre_erasure); + zend_try_compile_time_synthesize_monomorph(canonical); + zend_type_release(pre_erasure, /* persistent */ false); + return (zend_type) ZEND_TYPE_INIT_CLASS(canonical, 0, 0); + } + zend_type_release(pre_erasure, /* persistent */ false); + } ast = ast->child[0]; } /* Generic type parameter reference: erase to the parameter's bound (or `mixed` when unbounded). */ diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 785b2708b42b..f769be3f8da5 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -261,12 +261,14 @@ typedef struct _zend_turbofish_args_entry { ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count); ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); +ZEND_API zend_type_arg_table *zend_type_arg_table_capture_clone(const zend_type_arg_table *src); ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args(zend_execute_data *call, const zend_type *args_box, void **cache_slot); ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type); ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, uint32_t fetch_type); ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box); +ZEND_API bool zend_verify_generic_return_type(zend_execute_data *call, zval *retval_ptr); typedef union _zend_parser_stack_elem { zend_ast *ast; diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index c822695a7750..fad7301033b1 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -21,6 +21,7 @@ #define ZEND_EXECUTE_H #include "zend_compile.h" +#include "zend_closures.h" #include "zend_hash.h" #include "zend_operators.h" #include "zend_variables.h" @@ -338,7 +339,18 @@ static zend_always_inline void zend_vm_init_call_frame(zend_execute_data *call, Z_PTR(call->This) = object_or_called_scope; ZEND_CALL_INFO(call) = call_info; ZEND_CALL_NUM_ARGS(call) = num_args; - call->type_args = NULL; + /* Closures created inside a generic frame capture that frame's T-table. + * Propagate it onto the call frame so the body's RECV-time checks + * resolve outer T-refs. The closure owns the table (persisted=true); + * call-frame teardown skips destroying it. VERIFY_GENERIC_ARGUMENTS may + * still overwrite type_args later for explicit-turbofish calls. */ + if (UNEXPECTED(func->common.fn_flags & ZEND_ACC_CLOSURE)) { + const zend_closure *closure = + (const zend_closure *) ZEND_CLOSURE_OBJECT(func); + call->type_args = closure->captured_type_args; + } else { + call->type_args = NULL; + } } static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, uint32_t call_info, zend_function *func, uint32_t num_args, void *object_or_called_scope) diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index b3cb93e81400..e09f42b58a14 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1012,6 +1012,26 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_ if (func->type == ZEND_USER_FUNCTION) { uint32_t orig_jit_trace_num = EG(jit_trace_num); + /* C-dispatched calls (usort, array_map, Closure::call, etc.) don't go + * through DO_FCALL, so the closure-side reified arg check that lives + * there is bypassed. Run it here for closures whose captured T-table + * was propagated by zend_vm_init_call_frame. Must fire before + * zend_init_func_execute_data, because that relocates variadic args + * past the CV slots and the helper would then read UNDEF positions. */ + if (UNEXPECTED(call->type_args + && (func->common.fn_flags & ZEND_ACC_CLOSURE))) { + if (!zend_verify_generic_arg_types(call, NULL)) { + zend_vm_stack_free_args(call); + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + zend_vm_stack_free_call_frame(call); + EG(fake_scope) = orig_fake_scope; + zend_release_fcall_info_cache(fci_cache); + return SUCCESS; + } + } + zend_init_func_execute_data(call, &func->op_array, fci->retval); ZEND_OBSERVER_FCALL_BEGIN(call); zend_execute_ex(call); diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 2aa55ee5d4f1..f4abdfa02538 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -42,6 +42,7 @@ static void zend_check_generic_link_bounds( const char *clause, zend_class_entry *ce); static void zend_substitute_trait_method_arg_info( zend_function *new_fn, const zend_function *orig_fn, + const zend_class_entry *using_ce, const zend_type *bind_args, uint32_t bind_arity); static zend_arg_info *zend_clone_arg_info_block( const zend_arg_info *orig_block, uint32_t total); @@ -77,7 +78,7 @@ static void ZEND_COLD emit_incompatible_method_error( const zend_function *parent, const zend_class_entry *parent_scope, inheritance_status status); -static void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent); +ZEND_API void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent); static void zend_type_list_copy_ctor( zend_type *const parent_type, @@ -101,9 +102,48 @@ static void zend_type_list_copy_ctor( } ZEND_TYPE_LIST_FOREACH_END(); } -static void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent) { +static void zend_type_named_with_args_copy_ctor( + zend_type *const parent_type, bool use_arena, bool persistent) +{ + const zend_type_named_with_args *src = ZEND_TYPE_NAMED_WITH_ARGS(*parent_type); + size_t size = ZEND_TYPE_NAMED_WITH_ARGS_SIZE(src->count); + zend_type_named_with_args *dst = use_arena + ? zend_arena_alloc(&CG(arena), size) : pemalloc(size, persistent); + memcpy(dst, src, size); + if (dst->name) { + zend_string_addref(dst->name); + } + ZEND_TYPE_SET_PTR(*parent_type, dst); + for (uint32_t i = 0; i < dst->count; i++) { + zend_type_copy_ctor(&dst->args[i], use_arena, persistent); + } +} + +static void zend_type_parameter_ref_copy_ctor( + zend_type *const parent_type, bool persistent) +{ + const zend_type_parameter_ref *src = ZEND_TYPE_TYPE_PARAMETER(*parent_type); + zend_type_parameter_ref *dst = pemalloc(sizeof(*dst), persistent); + *dst = *src; + if (dst->name) { + zend_string_addref(dst->name); + } + ZEND_TYPE_SET_PTR(*parent_type, dst); +} + +ZEND_API void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent) { if (ZEND_TYPE_HAS_LIST(*type)) { zend_type_list_copy_ctor(type, use_arena, persistent); + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + /* Deep-clone the NWA payload + recurse into args. Without this, + * a copied type shares the source's heap-allocated NWA and either + * dangles (when the source frees it) or double-frees (when both + * release paths run). */ + zend_type_named_with_args_copy_ctor(type, use_arena, persistent); + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + /* Same story for type-parameter refs: each clone needs its own + * heap-allocated ref so release paths don't collide. */ + zend_type_parameter_ref_copy_ctor(type, persistent); } else if (ZEND_TYPE_HAS_NAME(*type)) { zend_string_addref(ZEND_TYPE_NAME(*type)); } @@ -841,23 +881,117 @@ static bool zend_get_inheritance_binding( return false; } -/* Substitutes a bare class-scope T-ref with its bound argument. */ +/* Substitutes class-scope T-refs with their bound arguments. + * + * Handles three shapes: + * - a bare T-ref ("T" or "?T") — return the substituted leaf; + * - a union/intersection list — walk the list and substitute any class-scope + * T-refs found inside, folding scalar bindings into the outer scalar mask + * and keeping named/intersection bindings in the list; + * - anything else — return unchanged. + * + * This is what makes property types like `T|null` reify correctly. Without the + * recursive walk, a T living inside a union stays literal at runtime and the + * property type check rejects valid assignments with "of type T". */ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *args, uint32_t arity) { - if (!ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE || ref->index >= arity) { + return t; + } + + zend_type result = args[ref->index]; + if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + + return result; + } + + if (!ZEND_TYPE_HAS_LIST(t)) { return t; } - const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); - if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE || ref->index >= arity) { + const zend_type_list *src_list = ZEND_TYPE_LIST(t); + bool needs_rebuild = false; + for (uint32_t i = 0; i < src_list->num_types; i++) { + const zend_type *elem = &src_list->types[i]; + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*elem)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*elem); + if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE && ref->index < arity) { + needs_rebuild = true; + break; + } + } else if (ZEND_TYPE_HAS_LIST(*elem)) { + /* Nested list (DNF: intersection inside union, etc.) — recurse to + * see if there's a T-ref buried in there. */ + zend_type probe = zend_substitute_leaf_type_param(*elem, args, arity); + if (memcmp(&probe, elem, sizeof(zend_type)) != 0) { + needs_rebuild = true; + break; + } + } + } + if (!needs_rebuild) { return t; } - zend_type result = args[ref->index]; - if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { - ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + bool is_intersection = (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_INTERSECTION_BIT) != 0; + uint32_t carried_flags = ZEND_TYPE_FULL_MASK(t) & ~_ZEND_TYPE_KIND_MASK + & ~_ZEND_TYPE_LIST_BIT & ~_ZEND_TYPE_ARENA_BIT + & ~_ZEND_TYPE_UNION_BIT & ~_ZEND_TYPE_INTERSECTION_BIT; + uint32_t merged_mask = carried_flags; + + ALLOCA_FLAG(use_heap) + zend_type *out = (zend_type *) do_alloca(sizeof(zend_type) * src_list->num_types, use_heap); + uint32_t out_count = 0; + + for (uint32_t i = 0; i < src_list->num_types; i++) { + zend_type substituted = zend_substitute_leaf_type_param(src_list->types[i], args, arity); + + /* Keep complex elements (named types, intersection sublists, unresolved + * T-refs) in the list; their scalar contribution is also OR'd into the + * outer mask in case the substituted type carries a NULLABLE bit. */ + bool keeps_complex = ZEND_TYPE_HAS_LIST(substituted) + || ZEND_TYPE_HAS_NAME(substituted) + || ZEND_TYPE_HAS_LITERAL_NAME(substituted) + || ZEND_TYPE_HAS_TYPE_PARAMETER(substituted) + || ZEND_TYPE_HAS_NAMED_WITH_ARGS(substituted); + + if (keeps_complex) { + out[out_count++] = substituted; + } + merged_mask |= ZEND_TYPE_PURE_MASK(substituted); + } + + zend_type result; + if (out_count == 0) { + result = (zend_type) ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_FULL_MASK(result) |= merged_mask; + } else if (out_count == 1 && !is_intersection + && (merged_mask & _ZEND_TYPE_MAY_BE_MASK & ~_ZEND_TYPE_NULLABLE_BIT) == 0) { + /* Degenerate union "Foo" or "?Foo" — represent as a single name with + * the nullable bit, matching how the parser builds the same shape. */ + result = out[0]; + zend_type_copy_ctor(&result, /* use_arena */ true, /* persistent */ false); + if (merged_mask & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + } else { + zend_type_list *new_list = zend_arena_alloc(&CG(arena), ZEND_TYPE_LIST_SIZE(out_count)); + new_list->num_types = out_count; + for (uint32_t i = 0; i < out_count; i++) { + new_list->types[i] = out[i]; + zend_type_copy_ctor(&new_list->types[i], /* use_arena */ true, /* persistent */ false); + } + result = (zend_type) ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, new_list); + uint32_t kind_bit = is_intersection ? _ZEND_TYPE_INTERSECTION_BIT : _ZEND_TYPE_UNION_BIT; + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_LIST_BIT | _ZEND_TYPE_ARENA_BIT | kind_bit | merged_mask; } + free_alloca(out, use_heap); return result; } @@ -1679,7 +1813,7 @@ static zend_function *zend_maybe_substitute_inherited_method( memcpy(clone, parent_fn, sizeof(zend_op_array)); clone->op_array.fn_flags &= ~ZEND_ACC_IMMUTABLE; - zend_substitute_trait_method_arg_info(clone, parent_fn, bound_args, bound_arity); + zend_substitute_trait_method_arg_info(clone, parent_fn, ce, bound_args, bound_arity); free_alloca(bound_args, use_heap); if (clone->op_array.arg_info == parent_fn->op_array.arg_info) { return NULL; @@ -2437,6 +2571,15 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke clone->flags |= ZEND_ACC_GENERIC_CLONE; clone->type = sub; zend_type_copy_ctor(&clone->type, /* use_arena */ true, /* persistent */ false); + /* The shallow copy borrows the name pointer from + * parent_info but doesn't addref it. zend_strings are + * refcounted and downstream paths (e.g. when this + * clone is destroyed) release the name, which would + * underflow the parent's count and corrupt the + * string. Take our own reference. */ + if (clone->name) { + zend_string_addref(clone->name); + } if (hooks && (hooks[ZEND_PROPERTY_HOOK_GET] || hooks[ZEND_PROPERTY_HOOK_SET])) { zend_function **clone_hooks = zend_arena_alloc( @@ -3407,8 +3550,39 @@ static zend_arg_info *zend_clone_arg_info_block(const zend_arg_info *orig_block, return new_block; } +/* After substitution lands on a CLASS_LIKE T-ref into the using class itself + * (e.g., `use Holder` in `class Box` rewrites Holder's X to + * Box's T), the substituted T-ref must be erased to the using class's + * declared bound. Without this, the trait's original "unbound X → mixed" + * erasure leaks into the using class's signature and the runtime + * type-check accepts values that violate the bound. Returns the original + * type when the ref isn't a using-class T-ref we can erase. */ +static zend_type zend_erase_using_class_t_ref( + zend_type t, const zend_class_entry *using_ce) +{ + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { + return t; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE + || !using_ce->generic_parameters + || ref->index >= using_ce->generic_parameters->count) { + return t; + } + const zend_generic_parameter *gp = + &using_ce->generic_parameters->parameters[ref->index]; + zend_type erased = ZEND_TYPE_IS_SET(gp->bound) + ? gp->bound + : (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ANY); + if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(erased) |= _ZEND_TYPE_NULLABLE_BIT; + } + return erased; +} + static void zend_substitute_trait_method_arg_info( zend_function *new_fn, const zend_function *orig_fn, + const zend_class_entry *using_ce, const zend_type *bind_args, uint32_t bind_arity) { if (orig_fn->type != ZEND_USER_FUNCTION) return; @@ -3448,6 +3622,7 @@ static void zend_substitute_trait_method_arg_info( const zend_type *pre = orig_op->generic_types->return_type; if (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre)) { zend_type sub = zend_substitute_leaf_type_param(*pre, bind_args, bind_arity); + sub = zend_erase_using_class_t_ref(sub, using_ce); if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { if (!new_block) { new_block = zend_clone_arg_info_block(orig_block, total); @@ -3474,6 +3649,7 @@ static void zend_substitute_trait_method_arg_info( } zend_type sub = zend_substitute_leaf_type_param(*pre_erasure, bind_args, bind_arity); + sub = zend_erase_using_class_t_ref(sub, using_ce); if (ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { continue; } @@ -3721,7 +3897,7 @@ static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_ } if (bind_args) { - zend_substitute_trait_method_arg_info(new_fn, fn, bind_args, bind_arity); + zend_substitute_trait_method_arg_info(new_fn, fn, ce, bind_args, bind_arity); } free_alloca(default_args, use_heap); @@ -6994,7 +7170,13 @@ ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( base = zend_lookup_class_ex(base_name, NULL, flags & ~ZEND_FETCH_CLASS_NO_AUTOLOAD); } zend_string_release(base_name); - if (!base || !base->generic_parameters) return NULL; + if (!base) return NULL; + if (!base->generic_parameters) { + zend_throw_error(NULL, + "Type arguments are not allowed on non-generic class %s", + ZSTR_VAL(base->name)); + return NULL; + } zend_monomorph_parser parser = { .p = ZSTR_VAL(name) + lt_pos + 1, diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index ff214ceab4db..0bb92414d299 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -42,6 +42,7 @@ typedef struct _zend_inheritance_binding_cache { ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface); ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *parent_ce, bool checked); +ZEND_API void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent); ZEND_API zend_inheritance_status zend_check_generic_arg_satisfies_bound( zend_class_entry *arg_scope, zend_type arg, zend_class_entry *bound_scope, zend_type bound); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 97f0cc7d1d0e..d9eb99789085 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -24,6 +24,7 @@ #include "zend_compile.h" #include "zend_extensions.h" #include "zend_API.h" +#include "zend_inheritance.h" #include "zend_sort.h" #include "zend_constants.h" #include "zend_observer.h" @@ -350,6 +351,33 @@ ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table) { efree(table); } +/* Clone a type-arg table for long-lived capture (e.g., closure capture). All + * entries are materialised into `owned_type` so the resulting table owns its + * type contents and doesn't depend on the source's lifetime. Returned table + * is marked persisted so frame teardown leaves it alone; the holder is + * responsible for clearing persisted and destroying when it's done. */ +ZEND_API zend_type_arg_table *zend_type_arg_table_capture_clone(const zend_type_arg_table *src) { + if (!src) { + return NULL; + } + zend_type_arg_table *dst = zend_type_arg_table_alloc(src->count); + dst->persisted = true; + for (uint32_t i = 0; i < src->count; i++) { + const zend_type_arg_entry *se = &src->entries[i]; + zend_type_arg_entry *de = &dst->entries[i]; + if (se->name) { + de->name = zend_string_copy(se->name); + } + const zend_type *src_type = zend_type_arg_entry_type(se); + if (src_type && ZEND_TYPE_IS_SET(*src_type)) { + zend_type owned = *src_type; + zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); + de->owned_type = owned; + } + } + return dst; +} + /* Always returns the canonical name of the bound type — class names, monomorph * applications (`Box`), scalars (`int`, `string`), and unions/intersections. * Callers that only care about class-name dispatch should themselves check @@ -741,6 +769,14 @@ ZEND_API void destroy_zend_class(zval *zv) zend_hash_release(prop_info->attributes); } free(prop_info); + } else if (prop_info->flags & ZEND_ACC_GENERIC_CLONE) { + /* Cross-class generic clones (e.g., Holder's property + * cloned into the Holder monomorph) take their own + * reference on the borrowed name string in + * do_inherit_property; release it here on the holder's + * destruction so the count balances. The prop_info struct + * itself is arena-allocated and freed in bulk. */ + zend_string_release(prop_info->name); } } ZEND_HASH_FOREACH_END(); zend_hash_destroy(&ce->properties_info); @@ -826,6 +862,32 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) { uint32_t i; + /* VERIFY/INSTALL_GENERIC_ARGS may have stashed a zend_type_arg_table* in + * their cache slot, marked persisted so per-frame teardown leaves it + * alone. The cache slot owns that allocation; release the contained + * tables for every op_array, regardless of whether the cache buffer + * itself is heap- or arena-allocated. */ + if (op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { + char *cache_buf = (char *) ZEND_MAP_PTR_GET(op_array->run_time_cache); + if (cache_buf) { + for (uint32_t op_idx = 0; op_idx < op_array->last; op_idx++) { + const zend_op *op = &op_array->opcodes[op_idx]; + if ((op->opcode == ZEND_VERIFY_GENERIC_ARGUMENTS + || op->opcode == ZEND_INSTALL_GENERIC_ARGS) + && op->result.num) { + void **cache_slot = (void **) (cache_buf + op->result.num); + zend_type_arg_table *t = (zend_type_arg_table *) cache_slot[0]; + if (t) { + t->persisted = false; + zend_type_arg_table_destroy(t); + cache_slot[0] = NULL; + cache_slot[1] = NULL; + } + } + } + } + } + if ((op_array->fn_flags & ZEND_ACC_HEAP_RT_CACHE) && ZEND_MAP_PTR(op_array->run_time_cache)) { efree(ZEND_MAP_PTR(op_array->run_time_cache)); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index b2193b2986f3..272871bad9ea 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4373,6 +4373,33 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER)) ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -4491,6 +4518,18 @@ ZEND_VM_COLD_CONST_HANDLER(124, ZEND_VERIFY_RETURN_TYPE, CONST|TMP|VAR|UNUSED|CV ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -4869,7 +4908,11 @@ ZEND_VM_HANDLER(107, ZEND_CATCH, CONST|UNUSED, JMP_ADDR, LAST_CATCH|CACHE_SLOT) void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { catch_ce = (zend_class_entry*)slot[2]; } else { catch_ce = zend_fetch_class(NULL, @@ -8184,7 +8227,11 @@ ZEND_VM_C_LABEL(try_instanceof): void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -8497,6 +8544,13 @@ ZEND_VM_HANDLER(142, ZEND_DECLARE_LAMBDA_FUNCTION, CONST, NUM) zend_create_closure(EX_VAR(opline->result.var), func, EX(func)->op_array.scope, called_scope, object); + /* If we're inside a generic frame, snapshot its T-table onto the new + * closure so the closure body resolves outer T-refs against the + * binding in effect at lambda-declaration time. */ + if (UNEXPECTED(EX(type_args) != NULL)) { + zend_closure_capture_type_args(EX_VAR(opline->result.var), EX(type_args)); + } + ZEND_VM_NEXT_OPCODE(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 824982c818dd..bacfd5f2b3bd 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -1974,6 +1974,33 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_D ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -2109,6 +2136,33 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_D ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -2244,6 +2298,33 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -5200,7 +5281,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_CO void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { catch_ce = (zend_class_entry*)slot[2]; } else { catch_ce = zend_fetch_class(NULL, @@ -5978,6 +6063,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_DECLARE_LAMBD zend_create_closure(EX_VAR(opline->result.var), func, EX(func)->op_array.scope, called_scope, object); + /* If we're inside a generic frame, snapshot its T-table onto the new + * closure so the closure body resolves outer T-refs against the + * binding in effect at lambda-declaration time. */ + if (UNEXPECTED(EX(type_args) != NULL)) { + zend_closure_capture_type_args(EX_VAR(opline->result.var), EX(type_args)); + } + ZEND_VM_NEXT_OPCODE(); } @@ -11258,6 +11350,18 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -19758,7 +19862,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -21429,7 +21537,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -21647,6 +21759,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_RETURN ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -21973,7 +22097,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -29854,6 +29982,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_RETURN ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -33092,7 +33232,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_UN void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { catch_ce = (zend_class_entry*)slot[2]; } else { catch_ce = zend_fetch_class(NULL, @@ -37360,6 +37504,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_RETURN ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -45124,7 +45280,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -48962,7 +49122,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -49791,6 +49955,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_RETURN ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -50359,7 +50535,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -55406,6 +55586,33 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FCA ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -55541,6 +55748,33 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FCA ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -55676,6 +55910,33 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FC ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -58516,7 +58777,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_CONST_T void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { catch_ce = (zend_class_entry*)slot[2]; } else { catch_ce = zend_fetch_class(NULL, @@ -59294,6 +59559,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DECLARE_LAMBDA_FUN zend_create_closure(EX_VAR(opline->result.var), func, EX(func)->op_array.scope, called_scope, object); + /* If we're inside a generic frame, snapshot its T-table onto the new + * closure so the closure body resolves outer T-refs against the + * binding in effect at lambda-declaration time. */ + if (UNEXPECTED(EX(type_args) != NULL)) { + zend_closure_capture_type_args(EX_VAR(opline->result.var), EX(type_args)); + } + ZEND_VM_NEXT_OPCODE(); } @@ -64472,6 +64744,18 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIF ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -72972,7 +73256,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -74643,7 +74931,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -74761,6 +75053,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_RETURN_TYPE ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -75087,7 +75391,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -82968,6 +83276,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_RETURN_TYPE ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -86206,7 +86526,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_UNUSED_ void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { catch_ce = (zend_class_entry*)slot[2]; } else { catch_ce = zend_fetch_class(NULL, @@ -90474,6 +90798,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_RETURN_TYPE ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -98238,7 +98574,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -102076,7 +102416,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); @@ -102803,6 +103147,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_RETURN_TYPE ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -103371,7 +103727,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV void **slot = CACHE_ADDR(opline->extended_value); uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; zend_class_entry *cur_scope = zend_get_called_scope(execute_data); - if (EXPECTED((uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { ce = (zend_class_entry*)slot[2]; } else { ce = zend_fetch_class(NULL, opline->op2.num); diff --git a/bench/bench_reification.php b/bench/bench_reification.php index 5b1b600093d6..7bbb3ac5ff03 100644 --- a/bench/bench_reification.php +++ b/bench/bench_reification.php @@ -396,6 +396,421 @@ function bench_instanceof_concrete(int $n): void { $inst_concrete_ns = bench("instanceof Container (cache slot)", 'bench_instanceof_concrete'); compare("delta: instanceof T vs instanceof bare", $inst_t_ns, "T", $inst_concrete_ns, "bare"); +// -------------------------------------------------------------------------- +// Scenario 11: depth scaling for nested monomorph instantiation. +// All monomorphs are synthesized + cached on first use; the hot loop is +// supposed to hit the class-table lookup at pointer-compare speed regardless +// of nesting depth. If the cost grows with depth in the hot path, something +// is being recomputed per call (canonical-name string build, arg-table +// rebuild, etc.). +// -------------------------------------------------------------------------- +class L1 {} +class L2 {} +class L3 {} +class L4 {} + +function deep_new_d1(int $n): void { + for ($i = 0; $i < $n; $i++) { new L1::(); } +} +function deep_new_d2(int $n): void { + for ($i = 0; $i < $n; $i++) { new L1::>(); } +} +function deep_new_d3(int $n): void { + for ($i = 0; $i < $n; $i++) { new L1::>>(); } +} +function deep_new_d4(int $n): void { + for ($i = 0; $i < $n; $i++) { new L1::>>>(); } +} + +// -------------------------------------------------------------------------- +// Scenario 12: composite type arguments (union, intersection, DNF). +// Canonicalization sorts the alternatives, so equivalent shapes converge on +// the same monomorph name. +// -------------------------------------------------------------------------- +interface Ifc1 {} +interface Ifc2 {} +class Holder1 {} + +function comp_concrete(int $n): void { + for ($i = 0; $i < $n; $i++) { new Holder1::(); } +} +function comp_union(int $n): void { + for ($i = 0; $i < $n; $i++) { new Holder1::(); } +} +function comp_intersection(int $n): void { + for ($i = 0; $i < $n; $i++) { new Holder1::(); } +} +function comp_dnf(int $n): void { + for ($i = 0; $i < $n; $i++) { new Holder1::<(Ifc1&Ifc2)|int>(); } +} + +// -------------------------------------------------------------------------- +// Scenario 13: bounded inner parameter — the `A>` pattern where +// the inner C declares its own bound, so synthesizing the outer monomorph +// triggers a bound-conformance check on the inner application. +// -------------------------------------------------------------------------- +class FBound {} +class FSub extends FBound {} +class WithBound {} +class WithoutBound {} + +function nested_bounded_only(int $n): void { + for ($i = 0; $i < $n; $i++) { new WithBound::(); } +} +function nested_outer_around_bounded(int $n): void { + for ($i = 0; $i < $n; $i++) { new WithoutBound::>(); } +} +function nested_intersection_with_bounded(int $n): void { + for ($i = 0; $i < $n; $i++) { new WithoutBound::>(); } +} + +// -------------------------------------------------------------------------- +// Scenario 14: instanceof on deeply-nested monomorphs. The polymorphic +// inline cache should make depth irrelevant after warmup — the slot just +// caches the resolved ce. +// -------------------------------------------------------------------------- +function deep_instanceof_d1(int $n): void { + $box = new L1::(); + for ($i = 0; $i < $n; $i++) { $_ = $box instanceof L1; } +} +function deep_instanceof_d3(int $n): void { + $box = new L1::>>(); + for ($i = 0; $i < $n; $i++) { $_ = $box instanceof L1>>; } +} +function deep_instanceof_d3_mismatch(int $n): void { + $box = new L1::>>(); + for ($i = 0; $i < $n; $i++) { $_ = $box instanceof L1>>; } +} +function deep_instanceof_d3_bare(int $n): void { + $box = new L1::>>(); + for ($i = 0; $i < $n; $i++) { $_ = $box instanceof L1; } +} + +section("11. Depth scaling — nested monomorph `new`"); +$d1_ns = bench("new L1 (depth 1)", 'deep_new_d1'); +$d2_ns = bench("new L1> (depth 2)", 'deep_new_d2'); +$d3_ns = bench("new L1>> (depth 3)", 'deep_new_d3'); +$d4_ns = bench("new L1>>> (depth 4)", 'deep_new_d4'); +compare("delta: depth 2 vs depth 1", $d2_ns, "d2", $d1_ns, "d1"); +compare("delta: depth 3 vs depth 1", $d3_ns, "d3", $d1_ns, "d1"); +compare("delta: depth 4 vs depth 1", $d4_ns, "d4", $d1_ns, "d1"); + +section("12. Composite type-argument shapes"); +$cc_ns = bench("new Holder1", 'comp_concrete'); +$cu_ns = bench("new Holder1", 'comp_union'); +$ci_ns = bench("new Holder1", 'comp_intersection'); +$cd_ns = bench("new Holder1<(Ifc1&Ifc2)|int>", 'comp_dnf'); +compare("delta: union vs concrete", $cu_ns, "uni", $cc_ns, "cnc"); +compare("delta: intersection vs concrete", $ci_ns, "ix", $cc_ns, "cnc"); +compare("delta: DNF vs concrete", $cd_ns, "dnf", $cc_ns, "cnc"); + +section("13. Bounded inner parameter (`A>` pattern)"); +$nb_ns = bench("new WithBound (just the inner)", 'nested_bounded_only'); +$nob_ns = bench("new WithoutBound>", 'nested_outer_around_bounded'); +$nix_ns = bench("new WithoutBound>", 'nested_intersection_with_bounded'); +compare("delta: outer wraps bounded vs inner alone", $nob_ns, "out", $nb_ns, "inner"); +compare("delta: intersection w/bound vs outer alone", $nix_ns, "ix", $nob_ns, "out"); + +section("14. Deep `instanceof` (PIC vs depth)"); +$di1_ns = bench("instanceof L1 (depth 1)", 'deep_instanceof_d1'); +$di3_ns = bench("instanceof L1>> (depth 3, match)", 'deep_instanceof_d3'); +$dim_ns = bench("instanceof L1>> (depth 3, mismatch)", 'deep_instanceof_d3_mismatch'); +$dib_ns = bench("instanceof L1 (bare, hits ancestor)", 'deep_instanceof_d3_bare'); +compare("delta: depth 3 match vs depth 1 match", $di3_ns, "d3", $di1_ns, "d1"); +compare("delta: depth 3 mismatch vs depth 3 match", $dim_ns, "mis", $di3_ns, "mat"); +compare("delta: bare ancestor vs depth 3 match", $dib_ns, "bare", $di3_ns, "d3"); + +// -------------------------------------------------------------------------- +// Scenario 15: deep parameter type check at RECV. The arg_info type is now +// the canonical monomorph (`Box>>`), so RECV's instanceof_function +// resolves and enforces the full identity — passing the wrong monomorph +// rejects, not silently accepts. After warmup the class-lookup cache makes +// the check the same speed as a non-generic typed param. +// -------------------------------------------------------------------------- +class DPlain {} +class DBox {} + +function takes_plain(DPlain $x): void {} +function takes_d1(DBox $x): void {} +function takes_d2(DBox> $x): void {} +function takes_d3(DBox>> $x): void {} +function takes_d4(DBox>>> $x): void {} + +function bench_param_plain(int $n): void { + $v = new DPlain(); + for ($i = 0; $i < $n; $i++) { takes_plain($v); } +} +function bench_param_d1(int $n): void { + $v = new DBox::(); + for ($i = 0; $i < $n; $i++) { takes_d1($v); } +} +function bench_param_d2(int $n): void { + $v = new DBox::>(); + for ($i = 0; $i < $n; $i++) { takes_d2($v); } +} +function bench_param_d3(int $n): void { + $v = new DBox::>>(); + for ($i = 0; $i < $n; $i++) { takes_d3($v); } +} +function bench_param_d4(int $n): void { + $v = new DBox::>>>(); + for ($i = 0; $i < $n; $i++) { takes_d4($v); } +} + +section("15. Deep parameter type check (RECV reifies the full monomorph)"); +$p_pl_ns = bench("f(DPlain \$x) (non-generic baseline)", 'bench_param_plain'); +$p_d1_ns = bench("f(DBox \$x) (depth 1)", 'bench_param_d1'); +$p_d2_ns = bench("f(DBox> \$x) (depth 2)", 'bench_param_d2'); +$p_d3_ns = bench("f(DBox>> \$x) (depth 3)", 'bench_param_d3'); +$p_d4_ns = bench("f(DBox>>> \$x) (depth 4)", 'bench_param_d4'); +compare("delta: deep d1 vs plain", $p_d1_ns, "d1", $p_pl_ns, "plain"); +compare("delta: deep d4 vs plain", $p_d4_ns, "d4", $p_pl_ns, "plain"); +compare("delta: deep d4 vs deep d1", $p_d4_ns, "d4", $p_d1_ns, "d1"); + +// -------------------------------------------------------------------------- +// Scenario 16: inference from a deep-typed value. The compiler can't +// destructure `Box` into a T-binding at the call site (only bare T +// params are inferable), but `id(T $x)` does pick up the value's full +// runtime monomorph as T. Compare to explicit turbofish with the same deep +// type, and to the equivalent non-generic call. +// -------------------------------------------------------------------------- +function id_gen(T $x): T { return $x; } + +function bench_infer_d1(int $n): void { + $v = new DBox::(); + for ($i = 0; $i < $n; $i++) { id_gen($v); } +} +function bench_infer_d3(int $n): void { + $v = new DBox::>>(); + for ($i = 0; $i < $n; $i++) { id_gen($v); } +} +function bench_turbofish_d3(int $n): void { + $v = new DBox::>>(); + for ($i = 0; $i < $n; $i++) { id_gen::>>>($v); } +} +function bench_plain_pass(int $n): void { + $v = new DPlain(); + for ($i = 0; $i < $n; $i++) { takes_plain($v); } +} + +section("16. Inference / turbofish with deep value"); +$inf1_ns = bench("id(DBox) — inferred", 'bench_infer_d1'); +$inf3_ns = bench("id(DBox>>) — inferred", 'bench_infer_d3'); +$tf3_ns = bench("id::>>>(\$v) — turbofish", 'bench_turbofish_d3'); +$plp_ns = bench("takes_plain(\$plain) — non-generic ref", 'bench_plain_pass'); +compare("delta: inferred d1 vs plain", $inf1_ns, "infD1", $plp_ns, "plain"); +compare("delta: inferred d3 vs plain", $inf3_ns, "infD3", $plp_ns, "plain"); +compare("delta: turbofish d3 vs inferred d3", $tf3_ns, "tf", $inf3_ns, "inf"); + +// -------------------------------------------------------------------------- +// Scenario 17: method dispatch on a deep generic receiver. The receiver's +// class is the monomorph (e.g., DReceiver>>); the method lookup +// goes through the monomorph's function table. Hot loops with the same +// receiver class should pay zero depth penalty after warmup. +// -------------------------------------------------------------------------- +class DReceiver { + public function tick(int $x): int { return $x + 1; } +} +class DReceiverPlain { + public function tick(int $x): int { return $x + 1; } +} + +function bench_recv_plain(int $n): void { + $r = new DReceiverPlain(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->tick($acc); } +} +function bench_recv_d1(int $n): void { + $r = new DReceiver::(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->tick($acc); } +} +function bench_recv_d3(int $n): void { + $r = new DReceiver::>>(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->tick($acc); } +} + +section("17. Method dispatch on a deep generic receiver"); +$mr_pl_ns = bench("DReceiverPlain->tick() (non-generic)", 'bench_recv_plain'); +$mr_d1_ns = bench("DReceiver->tick() (depth 1)", 'bench_recv_d1'); +$mr_d3_ns = bench("DReceiver>>->tick() (depth 3)", 'bench_recv_d3'); +compare("delta: receiver d1 vs plain", $mr_d1_ns, "d1", $mr_pl_ns, "plain"); +compare("delta: receiver d3 vs d1", $mr_d3_ns, "d3", $mr_d1_ns, "d1"); + +// -------------------------------------------------------------------------- +// Scenario 18: pass-through with deep param + deep return. Two RECV checks +// per call (param + return), each resolving a deep monomorph class. +// -------------------------------------------------------------------------- +function passthrough_plain(DPlain $x): DPlain { return $x; } +function passthrough_d3(DBox>> $x): DBox>> { return $x; } + +function bench_passthrough_plain(int $n): void { + $v = new DPlain(); + for ($i = 0; $i < $n; $i++) { passthrough_plain($v); } +} +function bench_passthrough_d3(int $n): void { + $v = new DBox::>>(); + for ($i = 0; $i < $n; $i++) { passthrough_d3($v); } +} + +section("18. Pass-through (param + return both deep)"); +$pt_pl_ns = bench("DPlain f(DPlain \$x): DPlain", 'bench_passthrough_plain'); +$pt_d3_ns = bench("DBox>> f(DBox>> \$x): same", 'bench_passthrough_d3'); +compare("delta: deep passthrough vs plain passthrough", $pt_d3_ns, "d3", $pt_pl_ns, "plain"); + +// ============================================================================ +// PATHOLOGICAL — looking for super-linear blowup. +// Each scenario varies one structural dimension while holding the others +// constant. If the cost grows worse than linearly in that dimension, the +// implementation has a problem worth flagging. +// ============================================================================ + +// -------------------------------------------------------------------------- +// Scenario 19: canonical-name traversal depth. Building "Box>>" +// touches every layer of the tree. Linear depth → linear cost is the goal. +// -------------------------------------------------------------------------- +class CB {} +function cn_d1(int $n): void { + for ($i = 0; $i < $n; $i++) { new CB::(); } +} +function cn_d3(int $n): void { + for ($i = 0; $i < $n; $i++) { new CB::>>(); } +} +function cn_d5(int $n): void { + for ($i = 0; $i < $n; $i++) { new CB::>>>>(); } +} +function cn_d8(int $n): void { + for ($i = 0; $i < $n; $i++) { new CB::>>>>>>>(); } +} + +// -------------------------------------------------------------------------- +// Scenario 20: wide DNF — many alternatives in a union of intersections. +// Pre-erasure value check loops over alternatives; worst case is O(width). +// -------------------------------------------------------------------------- +interface DA {} interface DB {} interface DC {} interface DD {} +interface DE {} interface DF {} interface DG {} interface DH {} +class Hits implements DA, DB, DC, DD, DE, DF, DG, DH {} +function dnf_w2(T $x): T { return $x; } +function dnf_w4(T $x): T { return $x; } +function dnf_w2_call(int $n): void { + $v = new Hits(); + for ($i = 0; $i < $n; $i++) { dnf_w2($v); } +} +function dnf_w4_call(int $n): void { + $v = new Hits(); + for ($i = 0; $i < $n; $i++) { dnf_w4($v); } +} + +// -------------------------------------------------------------------------- +// Scenario 21: diamond inheritance binding chain. D extends C>> +// resolution. Bench the act of constructing instances through the chain. +// -------------------------------------------------------------------------- +class CL1 {} +class CL2 extends CL1 {} +class CL3 extends CL2 {} +class CL4 extends CL3 {} +class CL5 extends CL4 {} +function chain_d2(int $n): void { + for ($i = 0; $i < $n; $i++) { new CL2::(); } +} +function chain_d3(int $n): void { + for ($i = 0; $i < $n; $i++) { new CL3::(); } +} +function chain_d4(int $n): void { + for ($i = 0; $i < $n; $i++) { new CL4::(); } +} +function chain_d5(int $n): void { + for ($i = 0; $i < $n; $i++) { new CL5::(); } +} + +// -------------------------------------------------------------------------- +// Scenario 22: polymorphic inline cache thrashing. The PIC at instanceof/ +// catch keys on (type_args generation, called_scope). A hot loop that +// switches the binding per iteration should miss the cache every time, +// turning O(1) into an O(class-table-lookup) cost per iteration. +// -------------------------------------------------------------------------- +class PicBox {} +function pic_stable(array $items): int { + $count = 0; + foreach ($items as $item) { + if ($item instanceof PicBox) { $count++; } + } + return $count; +} +/* Outer loop alternates the T binding per call — every entry into the inner + * loop sees a different type_args generation, so the PIC slot mismatches + * and re-resolves on the first item. The inner loop then runs warm. */ +function bench_pic_stable_int(int $n): void { + $items = array_fill(0, 16, new PicBox::()); + for ($i = 0; $i < $n; $i++) { + pic_stable::($items); + } +} +function bench_pic_alternating(int $n): void { + $items = array_fill(0, 16, new PicBox::()); + /* call alternates T between int and string — each call's args_box has + * a different cache key, so the per-call type_arg_table differs and + * the inner-loop PIC must re-resolve at least once per call. */ + for ($i = 0; $i < $n; $i++) { + if ($i & 1) { + pic_stable::($items); + } else { + pic_stable::($items); + } + } +} + +// -------------------------------------------------------------------------- +// Scenario 23: bound-conformance check on deeply-nested args. Each level of +// nesting may trigger an additional recursive call into the variance/bound +// machinery. +// -------------------------------------------------------------------------- +class BX {} +function bnd_d1(int $n): void { + for ($i = 0; $i < $n; $i++) { new BX::(); } +} +function bnd_d3(int $n): void { + for ($i = 0; $i < $n; $i++) { new BX::>>(); } +} +function bnd_d5(int $n): void { + for ($i = 0; $i < $n; $i++) { new BX::>>>>(); } +} + +section("19. Canonical-name traversal depth"); +$cn1_ns = bench("new CB (depth 1)", 'cn_d1'); +$cn3_ns = bench("new CB>> (depth 3)", 'cn_d3'); +$cn5_ns = bench("new CB>>>> (depth 5)", 'cn_d5'); +$cn8_ns = bench("new CB<...x8 levels...> (depth 8)", 'cn_d8'); +compare("delta: d3 vs d1 (3x deeper)", $cn3_ns, "d3", $cn1_ns, "d1"); +compare("delta: d5 vs d1 (5x deeper)", $cn5_ns, "d5", $cn1_ns, "d1"); +compare("delta: d8 vs d1 (8x deeper)", $cn8_ns, "d8", $cn1_ns, "d1"); + +section("20. Wide DNF bound — value check across union of intersections"); +$dnf2_ns = bench("dnf<(DA&DB)|(DC&DD)>(Hits) — 2 alts", 'dnf_w2_call'); +$dnf4_ns = bench("dnf<(DA&DB)|(DC&DD)|(DE&DF)|(DG&DH)> — 4", 'dnf_w4_call'); +compare("delta: 4 alts vs 2 alts (2x wider)", $dnf4_ns, "w4", $dnf2_ns, "w2"); + +section("21. Inheritance binding chain depth"); +$ch2_ns = bench("new CL2 (chain depth 2)", 'chain_d2'); +$ch3_ns = bench("new CL3 (chain depth 3)", 'chain_d3'); +$ch4_ns = bench("new CL4 (chain depth 4)", 'chain_d4'); +$ch5_ns = bench("new CL5 (chain depth 5)", 'chain_d5'); +compare("delta: chain 3 vs chain 2", $ch3_ns, "c3", $ch2_ns, "c2"); +compare("delta: chain 5 vs chain 2", $ch5_ns, "c5", $ch2_ns, "c2"); + +section("22. PIC thrashing (alternating T)"); +$pic_st_ns = bench("instanceof PicBox — stable T (PIC warm)", 'bench_pic_stable_int', ITERS_LIGHT); +$pic_alt_ns = bench("instanceof PicBox — alternating T", 'bench_pic_alternating', ITERS_LIGHT); +compare("delta: alternating vs stable", $pic_alt_ns, "alt", $pic_st_ns, "stable"); + +section("23. Bound-conformance depth"); +$bd1_ns = bench("new BX (depth 1, bound: object)", 'bnd_d1'); +$bd3_ns = bench("new BX>> (depth 3)", 'bnd_d3'); +$bd5_ns = bench("new BX<...x5 nested...> (depth 5)", 'bnd_d5'); +compare("delta: d3 vs d1 (3x deeper)", $bd3_ns, "d3", $bd1_ns, "d1"); +compare("delta: d5 vs d1 (5x deeper)", $bd5_ns, "d5", $bd1_ns, "d1"); + $opcache_loaded = extension_loaded('Zend OPcache'); $opcache_on = $opcache_loaded && function_exists('opcache_get_status') diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 40af0165887d..0e214a7350e3 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -8442,14 +8442,7 @@ static int zend_jit_isset_isempty_cv(zend_jit_ctx *jit, const zend_op *opline, u return 1; } -/* copy of hidden zend_closure */ -typedef struct _zend_closure { - zend_object std; - zend_function func; - zval this_ptr; - zend_class_entry *called_scope; - zif_handler orig_internal_handler; -} zend_closure; +/* zend_closure struct now exposed in zend_closures.h */ static int zend_jit_stack_check(zend_jit_ctx *jit, const zend_op *opline, uint32_t used_stack) { diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index 484cb5aa8fc9..09c52bff0cf5 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -17,6 +17,7 @@ #include "php_incomplete_class.h" #include "zend_portability.h" #include "zend_exceptions.h" +#include "zend_inheritance.h" /* {{{ reference-handling for unserializer: var_* */ #define VAR_ENTRIES_MAX 1018 /* 1024 - offsetof(php_unserialize_data, entries) / sizeof(void*) */ @@ -1195,7 +1196,8 @@ object ":" uiv ":" ["] { lc_name = zend_string_tolower(class_name); if(!unserialize_allowed_class(lc_name, var_hash)) { zend_string_release_ex(lc_name, 0); - if (!zend_is_valid_class_name(class_name)) { + if (!zend_is_valid_class_name(class_name) + && !zend_class_name_is_monomorph(class_name)) { zend_string_release_ex(class_name, 0); return 0; } @@ -1220,7 +1222,13 @@ object ":" uiv ":" ["] { break; } - if (!ZSTR_HAS_CE_CACHE(class_name) && !zend_is_valid_class_name(class_name)) { + /* Canonical monomorph names ("Box") contain characters that + * zend_is_valid_class_name() rejects. zend_lookup_class_ex() has a + * dedicated monomorphization hook for these names, so accept them + * here and let the lookup either synthesize the monomorph or fail. */ + if (!ZSTR_HAS_CE_CACHE(class_name) + && !zend_is_valid_class_name(class_name) + && !zend_class_name_is_monomorph(class_name)) { zend_string_release_ex(lc_name, 0); zend_string_release_ex(class_name, 0); return 0; From 6e6eb898ab83a6ce90fc8d53d9dd3de153481392 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 23 May 2026 10:16:03 +0200 Subject: [PATCH 12/30] fix synthesis inside generic methods Signed-off-by: Robert Landers --- Zend/zend_inheritance.c | 75 +++++++++++++++++++++++++++++++++++++++++ Zend/zend_inheritance.h | 9 +++++ Zend/zend_vm_def.h | 4 +-- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index f4abdfa02538..5b87831cb534 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -6664,6 +6664,81 @@ ZEND_API zend_class_entry *zend_get_defaults_monomorph(zend_class_entry *base) ZEND_UNREACHABLE(); } +/* When `new C::<...>(...)` is compiled inside a generic function/class, the + * turbofish args may reference enclosing-scope T parameters by ref. The op_array + * side-table stores those refs verbatim — at synth time they must be resolved + * against the executing frame's bindings, or the monomorph would carry literal + * "Box"-style args and its method arg_info would never resolve to a concrete + * type. Walks args[i]; for top-level T-refs, substitutes via the frame's + * function-level type_args (FUNCTION_LIKE) or the lexical class's monomorph + * descendant's generic_type_args (CLASS_LIKE). Out slots that didn't need + * substitution are copied verbatim so callers can pass out[] unconditionally. + * Returns false when any ref can't be resolved — caller should fall through to + * the existing synth-with-unresolved-args behavior so the diagnostic remains + * where it always was. */ +static bool zend_resolve_synth_args_against_frame( + const zend_type *args, uint32_t arity, zend_type *out) +{ + zend_execute_data *ex = EG(current_execute_data); + for (uint32_t i = 0; i < arity; i++) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(args[i])) { + out[i] = args[i]; + continue; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(args[i]); + const zend_type *resolved = NULL; + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + if (ex && ZEND_USER_CODE(ex->func->type) + && ex->type_args + && ref->index < ex->type_args->count) { + resolved = zend_type_arg_entry_type(&ex->type_args->entries[ref->index]); + } + } else { + /* Walk from called scope up to the direct child of the lexical + * class — that's the monomorph carrying the binding (see the + * matching walk in zend_resolve_generic_type_param). */ + if (ex) { + zend_class_entry *lexical = ex->func->common.scope; + zend_class_entry *cur = zend_get_called_scope(ex); + while (cur && cur->parent != lexical) { + cur = cur->parent; + } + if (cur && cur->generic_type_args + && ref->index < cur->generic_type_args->count) { + resolved = zend_type_arg_entry_type( + &cur->generic_type_args->entries[ref->index]); + } + } + } + if (!resolved || !ZEND_TYPE_IS_SET(*resolved)) { + return false; + } + out[i] = *resolved; + } + return true; +} + +/* Same as zend_synthesize_monomorph, but resolves TYPE_PARAMETER refs in args + * against the executing frame's T-tables first. Use this at runtime `new` + * sites where the args originate from a compile-time side-table that may + * reference enclosing-scope T's. Static callers (class-build extends/implements + * with already-resolved args) keep using zend_synthesize_monomorph directly. */ +ZEND_API zend_class_entry *zend_synthesize_monomorph_resolved( + zend_class_entry *base, const zend_type *args, uint32_t arity) +{ + if (arity == 0) { + return zend_synthesize_monomorph(base, args, arity); + } + zend_type resolved[ZEND_GENERIC_MAX_PARAMS]; + if (!zend_resolve_synth_args_against_frame(args, arity, resolved)) { + /* Fall through with the unresolved refs — produces the + * pre-existing "Box" monomorph + downstream TypeError that the + * caller's existing error path already handles. */ + return zend_synthesize_monomorph(base, args, arity); + } + return zend_synthesize_monomorph(base, resolved, arity); +} + ZEND_API zend_class_entry *zend_synthesize_monomorph( zend_class_entry *base, const zend_type *args, uint32_t arity) { diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index 0bb92414d299..5cc846048559 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -70,6 +70,15 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string ZEND_API zend_class_entry *zend_synthesize_monomorph( zend_class_entry *base, const zend_type *args, uint32_t arity); +/* Same as zend_synthesize_monomorph, but first resolves any TYPE_PARAMETER refs + * in args[] against the currently executing frame's bindings (function-level + * via EX()->type_args; class-level via the lexical class's monomorph descendant). + * Use at runtime `new` sites where the args originate from an op_array side-table + * compiled inside a generic scope and may name enclosing-scope T's by ref. + * Static-build callers with already-resolved args keep using the base. */ +ZEND_API zend_class_entry *zend_synthesize_monomorph_resolved( + zend_class_entry *base, const zend_type *args, uint32_t arity); + /* For a bare generic class `base`, synthesize (or return the cached) monomorph * built from the parameters' declared defaults. Returns NULL and throws Error * if any parameter has no default. If `base` is itself a monomorph (no diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 272871bad9ea..61a719a2f493 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9118,7 +9118,7 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { @@ -9184,7 +9184,7 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { From c54f218c8c941c68e79213f9a8f978adad918b30 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 24 May 2026 00:35:21 +0200 Subject: [PATCH 13/30] fix inheritance chaining Signed-off-by: Robert Landers fix inheritance chaining Signed-off-by: Robert Landers fix inheritance chaining Signed-off-by: Robert Landers --- .../errors/type_param_in_new_turbofish.phpt | 40 ++ ...impl_with_identity_type_param_binding.phpt | 24 + .../lsp/method_level_param_bound_subst.phpt | 31 ++ .../union_of_type_params_narrower_return.phpt | 25 + .../union_of_type_params_return_subst.phpt | 49 ++ .../violation_union_of_t_refs_widened.phpt | 17 + .../forwarded_t_ref_value_check.phpt | 2 +- .../monomorph_implements_substitution.phpt | 41 ++ .../named_with_args_t_in_property.phpt | 56 +++ .../new_turbofish_with_class_t_ref.phpt | 48 ++ .../new_turbofish_with_outer_t_ref.phpt | 61 +++ Zend/zend_compile.c | 63 ++- Zend/zend_compile.h | 1 + Zend/zend_inheritance.c | 428 ++++++++++++++++-- Zend/zend_vm_execute.h | 16 +- 15 files changed, 846 insertions(+), 56 deletions(-) create mode 100644 Zend/tests/generics/errors/type_param_in_new_turbofish.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/impl_with_identity_type_param_binding.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/method_level_param_bound_subst.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/union_of_type_params_narrower_return.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/union_of_type_params_return_subst.phpt create mode 100644 Zend/tests/generics/inheritance/lsp/violation_union_of_t_refs_widened.phpt create mode 100644 Zend/tests/generics/reification/monomorph_implements_substitution.phpt create mode 100644 Zend/tests/generics/reification/named_with_args_t_in_property.phpt create mode 100644 Zend/tests/generics/reification/new_turbofish_with_class_t_ref.phpt create mode 100644 Zend/tests/generics/reification/new_turbofish_with_outer_t_ref.phpt diff --git a/Zend/tests/generics/errors/type_param_in_new_turbofish.phpt b/Zend/tests/generics/errors/type_param_in_new_turbofish.phpt new file mode 100644 index 000000000000..ef408a982579 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_new_turbofish.phpt @@ -0,0 +1,40 @@ +--TEST-- +Errors: a forwarded TYPE_PARAMETER ref in a `new C::()` turbofish errors at the synth site when nothing pins T and its bound is not a class +--FILE-- +(...)` instead of +// being the class itself. Both must produce the same diagnostic, fired at the +// `new` site — not silently produce a broken-refs monomorph that crashes far +// from the cause when a method is later called on it. +final readonly class Box { + public function __construct(public array $items = []) {} +} + +function makeBox(): Box { + return new Box::([]); +} + +try { + makeBox(); + echo "no error??\n"; +} catch (Error $e) { + echo "ok: " . $e->getMessage() . "\n"; +} + +// A class-bound on the outer T gives a fallback target. +class Base {} +function makeBoxFromBound(): Box { + return new Box::([]); +} +$b = makeBoxFromBound(); +var_dump($b::class); + +// Turbofish supplied: no error, T resolves to the supplied type. +$b = makeBox::(); +var_dump($b::class); +?> +--EXPECT-- +ok: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class +string(9) "Box" +string(8) "Box" diff --git a/Zend/tests/generics/inheritance/lsp/impl_with_identity_type_param_binding.phpt b/Zend/tests/generics/inheritance/lsp/impl_with_identity_type_param_binding.phpt new file mode 100644 index 000000000000..8aa381658bea --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/impl_with_identity_type_param_binding.phpt @@ -0,0 +1,24 @@ +--TEST-- +Parametric LSP: child implementing `I` with its own TN forwarded to parent's TN +--FILE-- + { + public function getEdgesFrom(TN $from): array; +} + +class DG implements GI { + public function getEdgesFrom(TN $from): array { return []; } +} + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/lsp/method_level_param_bound_subst.phpt b/Zend/tests/generics/inheritance/lsp/method_level_param_bound_subst.phpt new file mode 100644 index 000000000000..c8b0fb55d328 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/method_level_param_bound_subst.phpt @@ -0,0 +1,31 @@ +--TEST-- +Parametric LSP: method-level type-parameter bound that references a class-scope T substitutes when the subclass binds T +--FILE-- + { function set(U $x) }` — the method's +// own U has a bound that's a class-scope T-ref. When `class B extends A` +// supplies T = string, the inheritance check on the inherited (or overridden) +// method must see U's bound substituted from T → string, otherwise the parent +// renders as `set(mixed $x)` (T erased) and the child's `set(U $x)` +// gets rejected as narrower than mixed. + +class A { + public function set(U $x): void {} +} + +class B extends A { + public function set(U $x): void {} +} + +class C { + public function set(U $x): void {} +} + +class D extends C { + public function set(U $x): void {} +} + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/lsp/union_of_type_params_narrower_return.phpt b/Zend/tests/generics/inheritance/lsp/union_of_type_params_narrower_return.phpt new file mode 100644 index 000000000000..a11fba5996cd --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/union_of_type_params_narrower_return.phpt @@ -0,0 +1,25 @@ +--TEST-- +Parametric LSP: child narrowing Tl|Tr return down to a single Tl is accepted +--FILE-- + { + abstract public function get(): T; +} + +class LeftOnly extends Base { + public function __construct(private Tl $value) {} + public function get(): Tl { return $this->value; } +} + +$a = new LeftOnly::("hi"); +var_dump($a->get()); + +$b = new LeftOnly::(42); +var_dump($b->get()); +?> +--EXPECT-- +string(2) "hi" +int(42) diff --git a/Zend/tests/generics/inheritance/lsp/union_of_type_params_return_subst.phpt b/Zend/tests/generics/inheritance/lsp/union_of_type_params_return_subst.phpt new file mode 100644 index 000000000000..bb561a56edf1 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/union_of_type_params_return_subst.phpt @@ -0,0 +1,49 @@ +--TEST-- +Parametric LSP: child returning union of two type parameters (Tl|Tr) satisfies parent's T when parent is bound to Tl|Tr +--FILE-- + with overrides returning Tl|Tr. + +abstract class Base { + abstract public function get(): T; +} + +class Pair extends Base { + public function __construct(private Tl|Tr $value) {} + public function get(): Tl|Tr { return $this->value; } +} + +$p = new Pair::("hi"); +var_dump($p->get()); + +$q = new Pair::(42); +var_dump($q->get()); + +// Also exercise the interface-implements path, which is what the Psl +// failure actually hit (UnionType extends Type which implements TypeInterface). +interface I<+T> { + public function read(mixed $v): T; +} + +abstract class IBase implements I { + abstract public function read(mixed $v): T; +} + +class Union extends IBase { + public function read(mixed $v): Tl|Tr { + if ($v instanceof Tl || $v instanceof Tr) return $v; + throw new TypeError("nope"); + } +} + +echo "loaded\n"; +?> +--EXPECT-- +string(2) "hi" +int(42) +loaded diff --git a/Zend/tests/generics/inheritance/lsp/violation_union_of_t_refs_widened.phpt b/Zend/tests/generics/inheritance/lsp/violation_union_of_t_refs_widened.phpt new file mode 100644 index 000000000000..0e83ce757c60 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_union_of_t_refs_widened.phpt @@ -0,0 +1,17 @@ +--TEST-- +Parametric LSP: child returning Tl|Tr|string (a widening of the parent's substituted Tl|Tr) is rejected +--FILE-- + { + abstract public function get(): T; +} + +class BadPair extends Base { + public function get(): Tl|Tr|string { throw new Exception(); } +} +?> +--EXPECTF-- +Fatal error: Declaration of BadPair::get(): Tl|Tr|string must be compatible with Base::get(): Tl|Tr in %s on line %d diff --git a/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt b/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt index 904ee2998f7e..08015a300682 100644 --- a/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt +++ b/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt @@ -81,7 +81,7 @@ object(Dog)#%d (0) { caught: id(): Argument #1 ($x) must be of type Dog, Cat given int(42) caught: id(): Argument #1 ($x) must be of type int, string given -caught: id(): Argument #1 ($x) must be of type int|string, array given +caught: id(): Argument #1 ($x) must be of type string|int, array given object(Both)#%d (0) { } caught: id(): Argument #1 ($x) must be of type Fooable&Barable, FooOnly given diff --git a/Zend/tests/generics/reification/monomorph_implements_substitution.phpt b/Zend/tests/generics/reification/monomorph_implements_substitution.phpt new file mode 100644 index 000000000000..f55fd47bd82f --- /dev/null +++ b/Zend/tests/generics/reification/monomorph_implements_substitution.phpt @@ -0,0 +1,41 @@ +--TEST-- +Reification: a monomorph of `B implements I` should report `is_a I` true +--FILE-- + implements I` is monomorphized as `B`, +// the implements binding has to be substituted so that `B` is also +// known to implement `I`. Otherwise `instanceof I` / +// `is_a(..., 'I')` returns false even though the type relationship +// holds, and property writes typed `I` reject `B` values. + +interface I<+T> {} +class B implements I {} + +// 1) Direct monomorph +$b = new B::(); +var_dump(is_a($b, 'I')); +var_dump(is_a($b, 'B')); +$mono = 'B'; +var_dump($b instanceof $mono); + +// 2) Non-generic subclass of a monomorph +class StrB extends B {} +$s = new StrB(); +var_dump(is_a($s, 'I')); +var_dump(is_a($s, 'B')); + +// 3) Assignment to a property typed I accepts a B +class Holder { + public I $val; +} +$h = new Holder::(); +$h->val = new B::(); +var_dump($h->val::class); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(9) "B" diff --git a/Zend/tests/generics/reification/named_with_args_t_in_property.phpt b/Zend/tests/generics/reification/named_with_args_t_in_property.phpt new file mode 100644 index 000000000000..7c06d2f76ce3 --- /dev/null +++ b/Zend/tests/generics/reification/named_with_args_t_in_property.phpt @@ -0,0 +1,56 @@ +--TEST-- +Reification: a property typed `I` (T inside a generic class type arg) substitutes T → binding for the monomorph +--FILE-- + $val` on a generic class. When the class is +// monomorphized (e.g. `new Box::()`), the property type should +// substitute T → string and the runtime check should accept values that +// satisfy `I`. The bug was that `zend_substitute_leaf_type_param` +// didn't recurse into NAMED_WITH_ARGS payloads, so the property stayed +// typed as the unsubstituted `I` and any assignment failed with +// "Cannot assign ... of type I". + +interface I<+T> {} + +class StrImpl implements I {} +class IntImpl implements I {} + +class Box { + public I $val; +} + +// Direct monomorph use. +$b = new Box::(); +$b->val = new StrImpl(); +var_dump($b->val::class); + +// Wrong-arg implementation rejected. +try { + $b->val = new IntImpl(); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +// Inherited subclass case: same substitution applies when the property is +// touched on an instance of a non-generic child. Use `mixed` on the ctor +// so the bad value reaches the property assignment site, exercising the +// inherited property's substituted type rather than the ctor's own type. +class StringBox extends Box { + public function __construct(mixed $val) { + $this->val = $val; + } +} +$sb = new StringBox(new StrImpl()); +var_dump($sb->val::class); + +try { + new StringBox(new IntImpl()); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +string(7) "StrImpl" +1: Cannot assign IntImpl to property %s::$val of type I +string(7) "StrImpl" +2: Cannot assign IntImpl to property %s::$val of type I diff --git a/Zend/tests/generics/reification/new_turbofish_with_class_t_ref.phpt b/Zend/tests/generics/reification/new_turbofish_with_class_t_ref.phpt new file mode 100644 index 000000000000..f1d7b6274db4 --- /dev/null +++ b/Zend/tests/generics/reification/new_turbofish_with_class_t_ref.phpt @@ -0,0 +1,48 @@ +--TEST-- +Reification: `new SelfClass::(...)` inside a generic class method resolves T against the instance's monomorph args at synth time +--FILE-- +(...)` +// using its own class-level T's. Runtime synth must walk the called scope +// up to the lexical class's monomorph descendant to resolve the binding. + +final readonly class Bag { + public function __construct(public array $items = []) {} + + public function withItem(T $item): Bag { + $items = $this->items; + $items[] = $item; + // Forward T explicitly — must resolve against the current instance's + // monomorph (Bag stays Bag, not Bag). + return new Bag::($items); + } +} + +$b = new Bag::([1, 2, 3]); +var_dump($b::class); +$b = $b->withItem(4); +var_dump($b::class); +$b = $b->withItem(5); +var_dump($b::class); + +$s = new Bag::(['a']); +var_dump($s::class); +$s = $s->withItem('b'); +var_dump($s::class); + +// Calling withItem on a bare-default Bag still works — forwarding +// resolves T to its (default) mixed binding from the monomorph. +$d = new Bag(); +var_dump($d::class); +$d = $d->withItem(42); +var_dump($d::class); +?> +--EXPECT-- +string(8) "Bag" +string(8) "Bag" +string(8) "Bag" +string(11) "Bag" +string(11) "Bag" +string(10) "Bag" +string(10) "Bag" diff --git a/Zend/tests/generics/reification/new_turbofish_with_outer_t_ref.phpt b/Zend/tests/generics/reification/new_turbofish_with_outer_t_ref.phpt new file mode 100644 index 000000000000..7bdfb9ac92de --- /dev/null +++ b/Zend/tests/generics/reification/new_turbofish_with_outer_t_ref.phpt @@ -0,0 +1,61 @@ +--TEST-- +Reification: `new Box::(...)` inside `function(...)` resolves T against the caller frame's T-table at synth time, not the literal ref +--FILE-- +" class with un-resolvable +// arg_info and runtime TypeErrors mentioning the literal parameter name). + +final readonly class Box { + public function __construct(public array $items = []) {} + public function get(TNode $key): mixed { return $this->items[$key] ?? null; } +} + +function make(): Box { + return new Box::([]); +} + +function withItem(Box $b, TNode $key, mixed $val): Box { + $items = $b->items; + $items[$key] = $val; + return new Box::($items); +} + +// Concrete turbofish on the outer call — inner `new Box::(...)` +// must resolve to , not . +$b = make::(); +var_dump($b::class); + +$b = withItem::($b, 'a', 1); +$b = withItem::($b, 'b', 2); +var_dump($b::class); + +// Method that's substituted on the monomorph should accept the bound key type. +var_dump($b->get('a')); +var_dump($b->get('b')); + +// Repeat with different turbofish — distinct monomorph, no stale binding leak. +$s = make::(); +var_dump($s::class); +$s = withItem::($s, 0, 'zero'); +var_dump($s::class); +var_dump($s->get(0)); + +// Defaults case: turbofish-less call falls back to declared defaults +// (mixed, mixed), and inner `new Box::(...)` resolves against +// those — producing Box, not Box. +$m = make(); +var_dump($m::class); +?> +--EXPECT-- +string(15) "Box" +string(15) "Box" +int(1) +int(2) +string(15) "Box" +string(15) "Box" +string(4) "zero" +string(16) "Box" diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6ae5745e110d..7f24af3222c6 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1532,14 +1532,43 @@ static zend_type zend_compile_pre_erasure_typename(zend_ast *ast) if (ast->kind == ZEND_AST_TYPE_UNION || ast->kind == ZEND_AST_TYPE_INTERSECTION) { zend_ast_list *list = zend_ast_get_list(ast); + bool is_union = ast->kind == ZEND_AST_TYPE_UNION; zend_type_list *type_list = emalloc(ZEND_TYPE_LIST_SIZE(list->children)); - type_list->num_types = list->children; + uint32_t out_count = 0; + uint32_t merged_scalar_mask = 0; for (uint32_t i = 0; i < list->children; i++) { - type_list->types[i] = zend_compile_pre_erasure_typename(list->child[i]); + zend_type member = zend_compile_pre_erasure_typename(list->child[i]); + if (is_union) { + /* Mirror the erased form's normalization: aggregate scalar bits + * to the outer mask so covariance checks see them, and drop + * scalar-only members from the list. Complex members (T-refs, + * named-with-args, nested lists, plain class names) stay in + * the list with their scalar bits cleared. */ + bool is_complex = ZEND_TYPE_HAS_LIST(member) + || ZEND_TYPE_HAS_NAME(member) + || ZEND_TYPE_HAS_LITERAL_NAME(member) + || ZEND_TYPE_HAS_TYPE_PARAMETER(member) + || ZEND_TYPE_HAS_NAMED_WITH_ARGS(member); + merged_scalar_mask |= ZEND_TYPE_PURE_MASK(member); + if (is_complex) { + ZEND_TYPE_FULL_MASK(member) &= ~_ZEND_TYPE_MAY_BE_MASK; + type_list->types[out_count++] = member; + } + } else { + type_list->types[out_count++] = member; + } + } + if (out_count == 0) { + /* All members folded into the scalar mask. */ + efree(type_list); + ZEND_TYPE_FULL_MASK(result) |= merged_scalar_mask; + } else { + type_list->num_types = out_count; + ZEND_TYPE_SET_PTR(result, type_list); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_LIST_BIT | + (is_union ? _ZEND_TYPE_UNION_BIT : _ZEND_TYPE_INTERSECTION_BIT) | + merged_scalar_mask; } - ZEND_TYPE_SET_PTR(result, type_list); - ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_LIST_BIT | - (ast->kind == ZEND_AST_TYPE_UNION ? _ZEND_TYPE_UNION_BIT : _ZEND_TYPE_INTERSECTION_BIT); } else if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { zend_ast *name_ast = ast->child[0]; zend_ast_list *args_list = zend_ast_get_list(ast->child[1]); @@ -3014,6 +3043,30 @@ ZEND_API bool zend_type_contains_type_parameter(zend_type type) return false; } +/* Like zend_type_contains_type_parameter, but only counts class-scope refs. + * Function-scope T-refs erase to their bound and aren't bound by class + * inheritance, so callers walking inheritance can ignore them. */ +ZEND_API bool zend_type_contains_class_scope_type_parameter(zend_type type) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + return ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE; + } + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_class_scope_type_parameter(nwa->args[i])) return true; + } + } + if (ZEND_TYPE_HAS_LIST(type)) { + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), member) { + if (zend_type_contains_class_scope_type_parameter(*member)) return true; + } ZEND_TYPE_LIST_FOREACH_END(); + } + return false; +} + ZEND_API zend_string *zend_generic_canonical_class_name( zend_string *base_name, const zend_type *args, uint32_t arity) { diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index f769be3f8da5..34ff61510c58 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1208,6 +1208,7 @@ ZEND_API zend_string *zend_type_to_canonical_string(zend_type type); ZEND_API zend_string *zend_generic_canonical_class_name( zend_string *base_name, const zend_type *args, uint32_t arity); ZEND_API bool zend_type_contains_type_parameter(zend_type type); +ZEND_API bool zend_type_contains_class_scope_type_parameter(zend_type type); /* class fetches */ #define ZEND_FETCH_CLASS_DEFAULT 0 diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 5b87831cb534..f38ad27f8351 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -909,6 +909,63 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a return result; } + /* Named-with-args (e.g. `I`): recurse into args. If every arg ends up + * ground, materialize the monomorph and return a plain CLASS type — that's + * the shape the runtime property/arg type checks already understand. If + * any T-ref remains, return a new NWA with the partially-substituted args. */ + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(t)) { + const zend_type_named_with_args *src_nwa = ZEND_TYPE_NAMED_WITH_ARGS(t); + bool needs_rebuild = false; + for (uint32_t i = 0; i < src_nwa->count; i++) { + zend_type probe = zend_substitute_leaf_type_param(src_nwa->args[i], args, arity); + if (memcmp(&probe, &src_nwa->args[i], sizeof(zend_type)) != 0) { + needs_rebuild = true; + break; + } + } + if (!needs_rebuild) { + return t; + } + + ALLOCA_FLAG(use_heap) + zend_type *new_args = (zend_type *) do_alloca(sizeof(zend_type) * src_nwa->count, use_heap); + bool all_concrete = true; + for (uint32_t i = 0; i < src_nwa->count; i++) { + new_args[i] = zend_substitute_leaf_type_param(src_nwa->args[i], args, arity); + if (zend_type_contains_type_parameter(new_args[i])) { + all_concrete = false; + } + } + + zend_type result; + if (all_concrete) { + /* All bound — fold to a plain CLASS reference to the monomorph. + * The class-lookup hook synthesizes it on demand if needed. */ + zend_string *canonical = zend_generic_canonical_class_name( + src_nwa->name, new_args, src_nwa->count); + result = (zend_type) ZEND_TYPE_INIT_CLASS(canonical, 0, 0); + } else { + /* Partial substitution — keep as NWA with substituted args. */ + size_t size = ZEND_TYPE_NAMED_WITH_ARGS_SIZE(src_nwa->count); + zend_type_named_with_args *new_nwa = zend_arena_alloc(&CG(arena), size); + new_nwa->name = zend_string_copy(src_nwa->name); + new_nwa->name_attr = src_nwa->name_attr; + new_nwa->count = src_nwa->count; + for (uint32_t i = 0; i < src_nwa->count; i++) { + new_nwa->args[i] = new_args[i]; + zend_type_copy_ctor(&new_nwa->args[i], /* use_arena */ true, /* persistent */ false); + } + ZEND_TYPE_SET_PTR(result, new_nwa); + ZEND_TYPE_FULL_MASK(result) = ZEND_TYPE_FULL_MASK(t); + } + + if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + free_alloca(new_args, use_heap); + return result; + } + if (!ZEND_TYPE_HAS_LIST(t)) { return t; } @@ -1199,10 +1256,12 @@ static bool zend_get_target_default_args( return true; } -/* If ce supplies type arguments to proto's declaring scope (directly, - * transitively, or via parameter defaults), returns proto's pre-erasure type - * with class-scope T-refs substituted. */ -static zend_type zend_substitute_proto_type( +/* Internal: substitute proto's pre-erasure against ce's binding without + * applying the top-level type-parameter fallback. May return a type that + * still has TYPE_PARAMETER refs at the top — callers use this to detect + * the "would fall back" case so the child side can mirror it. Returns + * `fallback` (unchanged) when no substitution applies at all. */ +static zend_type zend_substitute_proto_type_raw( zend_type fallback, const zend_type *pre_erasure, const zend_function *proto, @@ -1212,8 +1271,32 @@ static zend_type zend_substitute_proto_type( return fallback; } - if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure) - || ZEND_TYPE_TYPE_PARAMETER(*pre_erasure)->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + /* Top-level type-param ref of function-scope origin: dereference to the + * param's bound's pre-erasure. This lets `set(U $x)` substitute + * T → ce's binding in U's bound, so the inheritance check compares the + * substituted bound rather than the erased mixed. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure) + && ZEND_TYPE_TYPE_PARAMETER(*pre_erasure)->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pre_erasure); + if (!ZEND_USER_CODE(proto->common.type)) return fallback; + const zend_op_array *op = &proto->op_array; + if (!op->generic_parameters || ref->index >= op->generic_parameters->count) { + return fallback; + } + const zend_generic_parameter *p = &op->generic_parameters->parameters[ref->index]; + const zend_type *bound_pre = ZEND_TYPE_IS_SET(p->bound_pre_erasure) + ? &p->bound_pre_erasure + : (ZEND_TYPE_IS_SET(p->bound) ? &p->bound : NULL); + if (!bound_pre || !zend_type_contains_type_parameter(*bound_pre)) { + return fallback; + } + /* Recurse with the bound as the pre-erasure. The recursive call may + * also need to deref (chained method-level bounds), but loop detection + * isn't needed for inheritance because the user-visible bound is a + * finite expression. */ + return zend_substitute_proto_type_raw(fallback, bound_pre, proto, ce); + } + if (!zend_type_contains_type_parameter(*pre_erasure)) { return fallback; } @@ -1235,14 +1318,103 @@ static zend_type zend_substitute_proto_type( && !zend_get_target_default_args(proto_scope, args, cap, &arity)) { result = fallback; } else { - zend_type substituted = zend_substitute_leaf_type_param(*pre_erasure, args, arity); - result = ZEND_TYPE_HAS_TYPE_PARAMETER(substituted) ? fallback : substituted; + result = zend_substitute_leaf_type_param(*pre_erasure, args, arity); } free_alloca(args, use_heap); return result; } +/* If ce supplies type arguments to proto's declaring scope (directly, + * transitively, or via parameter defaults), returns proto's pre-erasure type + * with class-scope T-refs substituted. Applies a top-level fallback when the + * substituted result is still a bare TYPE_PARAMETER (couldn't be ground). */ +static zend_type zend_substitute_proto_type( + zend_type fallback, + const zend_type *pre_erasure, + const zend_function *proto, + zend_class_entry *ce) +{ + zend_type result = zend_substitute_proto_type_raw(fallback, pre_erasure, proto, ce); + /* If the result is *still* a bare type-parameter (couldn't be ground), + * fall back to the erased form. Compound types containing leftover + * type-param leaves are kept — the structural comparison handles them + * by identity. */ + return ZEND_TYPE_HAS_TYPE_PARAMETER(result) ? fallback : result; +} + +/* Returns the type the inheritance check should use for the child (fe) side. + * When the child has a pre-erasure stash and that stash carries class-scope + * shape the arg_info erased away (e.g. `Tl|Tr` collapsed to mixed because + * both members are unbounded class-scope type parameters), prefer the + * pre-erasure so the check sees the same structure that's substituted in + * for the parent side. Function-scope refs aren't bound by inheritance — + * they erase to their bound and we keep using the erased form. + * + * `proto_substituted_had_type_param` tells us whether the parent's + * substitution would have fallen back to its erased form (its substituted + * result was a bare TYPE_PARAMETER). When that's true and the child's + * pre-erasure is also a bare TYPE_PARAMETER, the child falls back too — + * keeping both sides at the erased mixed so the comparison stays symmetric. + * Otherwise the child uses its pre-erasure so structural compounds (unions + * like `Tl|Tr`, named-with-args like `I`) line up with the parent's + * substituted form. */ +static zend_type zend_resolve_fe_type( + zend_type fallback, + const zend_type *pre_erasure, + const zend_function *fe, + zend_class_entry *ce, + bool proto_substituted_had_type_param) +{ + if (!pre_erasure || !ZEND_TYPE_IS_SET(*pre_erasure)) { + return fallback; + } + /* Function-scope TYPE_PARAMETER ref: deref to the param's bound's + * pre-erasure so its class-scope content can still drive the comparison. + * Mirrors the symmetric deref in zend_substitute_proto_type_raw. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure) + && ZEND_TYPE_TYPE_PARAMETER(*pre_erasure)->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pre_erasure); + if (!ZEND_USER_CODE(fe->common.type)) return fallback; + const zend_op_array *op = &fe->op_array; + if (!op->generic_parameters || ref->index >= op->generic_parameters->count) { + return fallback; + } + const zend_generic_parameter *p = &op->generic_parameters->parameters[ref->index]; + const zend_type *bound_pre = ZEND_TYPE_IS_SET(p->bound_pre_erasure) + ? &p->bound_pre_erasure + : (ZEND_TYPE_IS_SET(p->bound) ? &p->bound : NULL); + if (!bound_pre) return fallback; + return zend_resolve_fe_type(fallback, bound_pre, fe, ce, proto_substituted_had_type_param); + } + if (!zend_type_contains_class_scope_type_parameter(*pre_erasure)) { + /* Pre-erasure has structure but no class-scope refs to substitute. + * If proto fell back (its substituted result was still a bare type + * param), fall back here too for symmetric mixed-vs-mixed compare; + * otherwise use the pre-erasure structure directly so unions like + * `string|int` compare properly. */ + if (proto_substituted_had_type_param + && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure)) { + return fallback; + } + return *pre_erasure; + } + /* Mirror the parent's fall-back: if proto fell back AND fe's pre-erasure + * is itself a bare TYPE_PARAMETER, fall back to the erased form too. */ + if (proto_substituted_had_type_param + && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure)) { + return fallback; + } + /* Immediate-child case: the pre-erasure type-param refs are already in + * fe_scope, which is the same scope the comparison runs in. */ + if (ce == fe->common.scope) { + return *pre_erasure; + } + /* Transitive: substitute the child's class-scope refs against ce's + * binding to fe's scope. */ + return zend_substitute_proto_type(fallback, pre_erasure, fe, ce); +} + static const zend_type *zend_get_param_pre_erasure(const zend_function *proto, uint32_t param_idx) { if (!ZEND_USER_CODE(proto->common.type)) return NULL; @@ -1260,11 +1432,10 @@ static const zend_type *zend_get_return_pre_erasure(const zend_function *proto) } static inheritance_status zend_do_perform_arg_type_hint_check( - zend_class_entry *fe_scope, const zend_arg_info *fe_arg_info, - zend_class_entry *proto_scope, const zend_arg_info *proto_arg_info, - zend_type proto_type) /* {{{ */ + zend_class_entry *fe_scope, zend_type fe_type, + zend_class_entry *proto_scope, zend_type proto_type) /* {{{ */ { - if (!ZEND_TYPE_IS_SET(fe_arg_info->type) || ZEND_TYPE_PURE_MASK(fe_arg_info->type) == MAY_BE_ANY) { + if (!ZEND_TYPE_IS_SET(fe_type) || ZEND_TYPE_PURE_MASK(fe_type) == MAY_BE_ANY) { /* Child with no type or mixed type is always compatible */ return INHERITANCE_SUCCESS; } @@ -1277,7 +1448,7 @@ static inheritance_status zend_do_perform_arg_type_hint_check( /* Contravariant type check is performed as a covariant type check with swapped * argument order. */ return zend_perform_covariant_type_check( - proto_scope, proto_type, fe_scope, fe_arg_info->type); + proto_scope, proto_type, fe_scope, fe_type); } /* }}} */ @@ -1349,14 +1520,19 @@ static inheritance_status zend_do_perform_implementation_check( } uint32_t proto_param_idx = i < proto_num_args ? i : proto_num_args - 1; - zend_type proto_type = zend_substitute_proto_type( + zend_type proto_raw = zend_substitute_proto_type_raw( proto_arg_info->type, zend_get_param_pre_erasure(proto, proto_param_idx), - proto, - ce - ); + proto, ce); + bool proto_fell_back = ZEND_TYPE_HAS_TYPE_PARAMETER(proto_raw); + zend_type proto_type = proto_fell_back ? proto_arg_info->type : proto_raw; + uint32_t fe_param_idx = i < fe_num_args ? i : fe_num_args - 1; + zend_type fe_type = zend_resolve_fe_type( + fe_arg_info->type, + zend_get_param_pre_erasure(fe, fe_param_idx), + fe, ce, proto_fell_back); local_status = zend_do_perform_arg_type_hint_check( - fe_scope, fe_arg_info, proto_scope, proto_arg_info, proto_type); + fe_scope, fe_type, proto_scope, proto_type); if (UNEXPECTED(local_status != INHERITANCE_SUCCESS)) { if (UNEXPECTED(local_status == INHERITANCE_ERROR)) { @@ -1386,12 +1562,19 @@ static inheritance_status zend_do_perform_implementation_check( return status; } - zend_type proto_return_type = zend_substitute_proto_type( + zend_type proto_raw_ret = zend_substitute_proto_type_raw( proto->common.arg_info[-1].type, zend_get_return_pre_erasure(proto), proto, ce); + bool proto_ret_fell_back = ZEND_TYPE_HAS_TYPE_PARAMETER(proto_raw_ret); + zend_type proto_return_type = proto_ret_fell_back + ? proto->common.arg_info[-1].type : proto_raw_ret; + zend_type fe_return_type = zend_resolve_fe_type( + fe->common.arg_info[-1].type, + zend_get_return_pre_erasure(fe), + fe, ce, proto_ret_fell_back); local_status = zend_perform_covariant_type_check( - fe_scope, fe->common.arg_info[-1].type, proto_scope, proto_return_type); + fe_scope, fe_return_type, proto_scope, proto_return_type); if (UNEXPECTED(local_status != INHERITANCE_SUCCESS)) { if (local_status == INHERITANCE_ERROR @@ -1455,9 +1638,20 @@ static ZEND_COLD zend_string *zend_get_function_declaration( } for (uint32_t i = 0; i < num_args;) { uint32_t param_idx = i < fptr->common.num_args ? i : fptr->common.num_args; - zend_type display_type = subst_ce - ? zend_substitute_proto_type(arg_info->type, zend_get_param_pre_erasure(fptr, param_idx), fptr, subst_ce) - : arg_info->type; + const zend_type *param_pre = zend_get_param_pre_erasure(fptr, param_idx); + zend_type display_type; + if (subst_ce) { + display_type = zend_substitute_proto_type(arg_info->type, param_pre, fptr, subst_ce); + } else if (param_pre && ZEND_TYPE_IS_SET(*param_pre) + && zend_type_contains_class_scope_type_parameter(*param_pre)) { + /* Render the pre-erasure shape so messages mirror what the + * inheritance check actually compared. Function-scope refs + * erase to their bound and aren't bound by inheritance, so + * we keep the erased rendering for those. */ + display_type = *param_pre; + } else { + display_type = arg_info->type; + } zend_append_type_hint(&str, scope, arg_info, display_type, false); if (ZEND_ARG_SEND_MODE(arg_info)) { smart_str_appendc(&str, '&'); @@ -1553,9 +1747,18 @@ static ZEND_COLD zend_string *zend_get_function_declaration( if (fptr->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { smart_str_appends(&str, ": "); const zend_arg_info *ret_info = fptr->common.arg_info - 1; - zend_type ret_display = subst_ce - ? zend_substitute_proto_type(ret_info->type, zend_get_return_pre_erasure(fptr), fptr, subst_ce) - : ret_info->type; + const zend_type *ret_pre = zend_get_return_pre_erasure(fptr); + zend_type ret_display; + if (subst_ce) { + ret_display = zend_substitute_proto_type(ret_info->type, ret_pre, fptr, subst_ce); + } else if (ret_pre && ZEND_TYPE_IS_SET(*ret_pre) + && zend_type_contains_class_scope_type_parameter(*ret_pre)) { + /* Mirror the pre-erasure shape so messages match what the + * inheritance check actually compared. */ + ret_display = *ret_pre; + } else { + ret_display = ret_info->type; + } zend_append_type_hint(&str, scope, ret_info, ret_display, true); } smart_str_0(&str); @@ -6671,11 +6874,12 @@ ZEND_API zend_class_entry *zend_get_defaults_monomorph(zend_class_entry *base) * "Box"-style args and its method arg_info would never resolve to a concrete * type. Walks args[i]; for top-level T-refs, substitutes via the frame's * function-level type_args (FUNCTION_LIKE) or the lexical class's monomorph - * descendant's generic_type_args (CLASS_LIKE). Out slots that didn't need - * substitution are copied verbatim so callers can pass out[] unconditionally. - * Returns false when any ref can't be resolved — caller should fall through to - * the existing synth-with-unresolved-args behavior so the diagnostic remains - * where it always was. */ + * descendant's generic_type_args (CLASS_LIKE). When the frame has no binding, + * falls back to the referenced parameter's class bound (matching the runtime + * fallback used by `new T()` / `instanceof T` resolution). Throws and returns + * NULL when neither a binding nor a class-bound fallback is available — same + * shape and message as the existing zend_resolve_generic_type_param error so + * users see one consistent diagnostic across `new T()` and `new C::()`. */ static bool zend_resolve_synth_args_against_frame( const zend_type *args, uint32_t arity, zend_type *out) { @@ -6687,11 +6891,13 @@ static bool zend_resolve_synth_args_against_frame( } const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(args[i]); const zend_type *resolved = NULL; + const zend_generic_parameter_list *params = NULL; if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { - if (ex && ZEND_USER_CODE(ex->func->type) - && ex->type_args - && ref->index < ex->type_args->count) { - resolved = zend_type_arg_entry_type(&ex->type_args->entries[ref->index]); + if (ex && ZEND_USER_CODE(ex->func->type)) { + if (ex->type_args && ref->index < ex->type_args->count) { + resolved = zend_type_arg_entry_type(&ex->type_args->entries[ref->index]); + } + params = ex->func->op_array.generic_parameters; } } else { /* Walk from called scope up to the direct child of the lexical @@ -6708,12 +6914,35 @@ static bool zend_resolve_synth_args_against_frame( resolved = zend_type_arg_entry_type( &cur->generic_type_args->entries[ref->index]); } + if (lexical) { + params = lexical->generic_parameters; + } } } - if (!resolved || !ZEND_TYPE_IS_SET(*resolved)) { - return false; + if (resolved && ZEND_TYPE_IS_SET(*resolved)) { + out[i] = *resolved; + continue; } - out[i] = *resolved; + /* No binding from the frame. Fall back to the referenced parameter's + * class bound — same fallback as `new T()` / `instanceof T`. A + * non-class bound (default `mixed`, scalar, union, etc.) has no + * class name to substitute, so synth cannot proceed: throw. */ + if (params && ref->index < params->count) { + zend_type bound = params->parameters[ref->index].bound; + if (ZEND_TYPE_HAS_NAME(bound)) { + out[i] = bound; + continue; + } + zend_throw_error(NULL, + "Cannot resolve generic type parameter %s at runtime: " + "no binding was supplied and its bound is not a class", + ZSTR_VAL(params->parameters[ref->index].name)); + } else { + zend_throw_error(NULL, + "Cannot resolve generic type parameter at runtime: " + "no binding was supplied and no parameter scope is available"); + } + return false; } return true; } @@ -6731,10 +6960,7 @@ ZEND_API zend_class_entry *zend_synthesize_monomorph_resolved( } zend_type resolved[ZEND_GENERIC_MAX_PARAMS]; if (!zend_resolve_synth_args_against_frame(args, arity, resolved)) { - /* Fall through with the unresolved refs — produces the - * pre-existing "Box" monomorph + downstream TypeError that the - * caller's existing error path already handles. */ - return zend_synthesize_monomorph(base, args, arity); + return NULL; } return zend_synthesize_monomorph(base, resolved, arity); } @@ -6921,6 +7147,124 @@ ZEND_API zend_class_entry *zend_synthesize_monomorph( } } + /* Substitute T-typed implements on the inherited interface chain. When + * the base declares `class B implements I` and is monomorphized as + * `B`, the inherited interfaces[] still points at the erased + * base interface I — so `instanceof I` returns false. Walk the + * base's parent chain too, since `class B extends A` where + * `class A implements I` needs the same substitution for B. + * For each generic ancestor, resolve the binding from `base` to that + * ancestor, substitute each implements entry's args, synthesize the + * corresponding interface monomorph, and add it to linked->interfaces. + * The standard `instanceof` walks both the parent chain and interfaces[], + * so the substituted forms become discoverable and the erased base + * interfaces stay reachable transitively. */ + { + ALLOCA_FLAG(extras_use_heap) + SET_ALLOCA_FLAG(extras_use_heap); + uint32_t max_extras = 0; + for (zend_class_entry *a = base; a; a = a->parent) { + max_extras += a->num_interfaces; + } + zend_class_entry **extras = max_extras + ? do_alloca(sizeof(zend_class_entry *) * max_extras, extras_use_heap) + : NULL; + uint32_t extra_count = 0; + + for (zend_class_entry *ancestor = base; ancestor; ancestor = ancestor->parent) { + if (!ancestor->generic_types || !ancestor->generic_types->implements + || !ancestor->generic_parameters) { + continue; + } + uint32_t a_cap = ancestor->generic_parameters->count; + if (a_cap == 0) continue; + + /* Resolve the binding from the synthesized monomorph (whose direct + * binding is to `base`) to `ancestor`. For the immediate-base case, + * the binding is the args we were called with. For deeper ancestors, + * compose base→ancestor's binding and then substitute its T-refs + * with the args we hold. */ + zend_type bound_args[ZEND_GENERIC_MAX_PARAMS]; + uint32_t bound_arity = 0; + if (ancestor == base) { + if (arity > ZEND_GENERIC_MAX_PARAMS) continue; + for (uint32_t k = 0; k < arity; k++) bound_args[k] = args[k]; + bound_arity = arity; + } else { + bool have = zend_get_inheritance_binding_full_cached( + base, ancestor, bound_args, ZEND_GENERIC_MAX_PARAMS, &bound_arity); + if (!have) continue; + for (uint32_t k = 0; k < bound_arity; k++) { + bound_args[k] = zend_substitute_leaf_type_param( + bound_args[k], args, arity); + } + } + + const HashTable *impl_table = ancestor->generic_types->implements; + for (uint32_t i = 0; i < ancestor->num_interfaces; i++) { + const zend_type *impl_args_t = (const zend_type *) zend_hash_index_find_ptr(impl_table, i); + if (!impl_args_t || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*impl_args_t)) { + continue; + } + const zend_type_named_with_args *impl_nwa = ZEND_TYPE_NAMED_WITH_ARGS(*impl_args_t); + + ALLOCA_FLAG(sub_use_heap) + zend_type *sub_args = do_alloca( + sizeof(zend_type) * impl_nwa->count, sub_use_heap); + bool all_ground = true; + for (uint32_t j = 0; j < impl_nwa->count; j++) { + sub_args[j] = zend_substitute_leaf_type_param( + impl_nwa->args[j], bound_args, bound_arity); + if (zend_type_contains_type_parameter(sub_args[j])) { + all_ground = false; + break; + } + } + if (!all_ground) { + free_alloca(sub_args, sub_use_heap); + continue; + } + zend_class_entry *iface_base = ancestor->interfaces + ? ancestor->interfaces[i] : NULL; + if (!iface_base) { + iface_base = zend_lookup_class(impl_nwa->name); + } + zend_class_entry *iface_mono = NULL; + if (iface_base && iface_base->generic_parameters) { + iface_mono = zend_synthesize_monomorph( + iface_base, sub_args, impl_nwa->count); + } + free_alloca(sub_args, sub_use_heap); + if (!iface_mono) continue; + + bool already = false; + for (uint32_t k = 0; k < linked->num_interfaces; k++) { + if (linked->interfaces[k] == iface_mono) { already = true; break; } + } + if (already) continue; + for (uint32_t k = 0; k < extra_count; k++) { + if (extras[k] == iface_mono) { already = true; break; } + } + if (already) continue; + extras[extra_count++] = iface_mono; + } + } + + if (extra_count > 0) { + uint32_t new_count = linked->num_interfaces + extra_count; + linked->interfaces = perealloc( + linked->interfaces, + sizeof(zend_class_entry *) * new_count, + linked->type == ZEND_INTERNAL_CLASS); + for (uint32_t k = 0; k < extra_count; k++) { + linked->interfaces[linked->num_interfaces + k] = extras[k]; + do_implement_interface(linked, extras[k]); + } + linked->num_interfaces = new_count; + } + if (extras) free_alloca(extras, extras_use_heap); + } + /* Substitute T-typed class constants. When the base declares `const T FOO`, * the inherited entry in the mono is a shared pointer to the base's * `zend_class_constant` with type T (erased to mixed). For the mono, diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index bacfd5f2b3bd..95197c71e7d9 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -22300,7 +22300,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { @@ -22366,7 +22366,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { @@ -37935,7 +37935,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { @@ -38001,7 +38001,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { @@ -75594,7 +75594,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { @@ -75660,7 +75660,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { @@ -91229,7 +91229,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { @@ -91295,7 +91295,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph(ce, nwa->args, nwa->count); + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); if (mono && mono != ce) { Z_OBJ_P(new_obj)->ce = mono; if (mono->constructor && call->func == ce->constructor) { From 24c47e1ef6b87646cfa6b9c71d8f2662875ae188 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 15 Jun 2026 18:17:33 +0700 Subject: [PATCH 14/30] cache resolved monomorph per call site for new X::<...> --- Zend/zend_compile.c | 127 +++++++++++++++++++++++++++++++++++++ Zend/zend_compile.h | 1 + Zend/zend_opcode.c | 14 +++-- Zend/zend_vm_def.h | 34 +--------- Zend/zend_vm_execute.h | 140 +++-------------------------------------- 5 files changed, 149 insertions(+), 167 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 7f24af3222c6..f4be8859e305 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -755,6 +755,133 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( return t; } +ZEND_API zend_class_entry *zend_get_called_scope(const zend_execute_data *ex); + +/* Cache key for a `new C::<...>` site's resolved monomorph. The monomorph is a + * pure function of (base ce, resolved type args). The args are a + * compile-time-constant side table for the opline; only TYPE_PARAMETER refs add + * runtime variability, resolved against the executing frame: + * - FUNCTION_LIKE ref -> the frame's type_args entry, keyed by its interned + * canonical-name pointer (stable per binding); + * - CLASS_LIKE ref -> the called scope's generic_type_args, fully + * determined by the called_scope ce pointer. + * The key is seeded with the base ce pointer so a cached monomorph for one base + * can never satisfy a different base (e.g. `new $dynamic::`). Returns 0 + * ("uncacheable") only when a needed binding is unavailable (error paths). */ +static uintptr_t zend_compute_new_mono_cache_key( + const zend_class_entry *base, const zend_type_named_with_args *nwa, + const zend_execute_data *ex) +{ + uintptr_t key = (uintptr_t) base * 0x9E3779B97F4A7C15ULL; + for (uint32_t i = 0; i < nwa->count; i++) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(nwa->args[i])) { + continue; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(nwa->args[i]); + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + if (!ex || !ex->type_args || ref->index >= ex->type_args->count) { + return 0; + } + zend_string *bound_name = ex->type_args->entries[ref->index].name; + if (!bound_name) { + return 0; + } + key = key * 0x100000001B3ULL + (uintptr_t) bound_name + i + 1; + } else { + zend_class_entry *cs = ex ? zend_get_called_scope(ex) : NULL; + if (!cs) { + return 0; + } + key = key * 0x100000001B3ULL + (uintptr_t) cs + i + 1; + } + } + if (key == 0) { + key = (uintptr_t) -1; + } + return key; +} + +static zend_always_inline void zend_generic_new_swap_ce( + zval *new_obj, zend_execute_data *call, zend_class_entry *ce, zend_class_entry *mono) +{ + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } +} + +/* + * Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. + * Use a call-site inline cache to avoid building the canonical name and performing the class-table loookup again. + * + * `new C::<...>` resolution with a call-site monomorph cache. Without the cache + * every instantiation rebuilds the canonical class name (smart_str + interning), + * lowercases it, and hashes EG(class_table) — all to rediscover the same + * monomorph. On a cache hit we skip the arity/bound check and the synthesis + * lookup entirely: the cached ce is swapped into the new object (and the pending + * constructor call) directly. On a miss we run the full path and, when the site + * is cacheable, stash the resolved monomorph keyed by the binding fingerprint. + * + * `do_checks` mirrors the VERIFY vs INSTALL split: VERIFY runs the runtime + * arity+bound check on a miss; INSTALL (statically pre-validated) does not. The + * cached `mono` lives in EG(class_table) for the whole request and the cache + * slot is request-local runtime-cache memory, so the two share a lifetime. */ +ZEND_API void zend_apply_generic_new( + zval *new_obj, zend_execute_data *call, const zend_type *args_box, + uint32_t arity, void **cache_slot, bool do_checks) +{ + zend_class_entry *ce = Z_OBJCE_P(new_obj); + const zend_type_named_with_args *nwa = + (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) + ? ZEND_TYPE_NAMED_WITH_ARGS(*args_box) : NULL; + + if (nwa && ce->generic_parameters && cache_slot) { + uintptr_t key = zend_compute_new_mono_cache_key(ce, nwa, EG(current_execute_data)); + if (key && cache_slot[0] && (uintptr_t) cache_slot[1] == key) { + zend_generic_new_swap_ce(new_obj, call, ce, (zend_class_entry *) cache_slot[0]); + return; + } + if (do_checks) { + zend_check_generic_new_arguments(ce, arity, args_box); + if (EG(exception)) { + return; + } + } + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); + if (!mono || EG(exception)) { + return; + } + if (key) { + cache_slot[0] = mono; + cache_slot[1] = (void *) key; + } + zend_generic_new_swap_ce(new_obj, call, ce, mono); + return; + } + + /* No turbofish args, or ce isn't generic, or no cache slot: preserve the + * original semantics (the arity check still fires to reject turbofish args + * passed to a non-generic class). */ + if (do_checks) { + zend_check_generic_new_arguments(ce, arity, args_box); + if (EG(exception)) { + return; + } + } + if (nwa && ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); + if (!EG(exception)) { + zend_generic_new_swap_ce(new_obj, call, ce, mono); + } + } +} + /* Slots left NULL mean "fall back to the parameter's bound". Order of resolution * for each slot: explicit turbofish arg → parameter's declared default → * value-directed inference from any argument whose pre-erasure type is a diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 34ff61510c58..a39face5fa58 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -196,6 +196,7 @@ ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast); ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box); ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint32_t arity, const zend_type *args_box); +ZEND_API void zend_apply_generic_new(zval *new_obj, zend_execute_data *call, const zend_type *args_box, uint32_t arity, void **cache_slot, bool do_checks); ZEND_API const zend_type *zend_generic_get_turbofish_args(const zend_op_array *caller_op_array, uint32_t args_id); ZEND_API struct _zend_turbofish_args_entry *zend_generic_get_turbofish_call_entry(const zend_op_array *caller_op_array, uint32_t args_id); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index d9eb99789085..bc64dbd1975a 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -862,11 +862,14 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) { uint32_t i; - /* VERIFY/INSTALL_GENERIC_ARGS may have stashed a zend_type_arg_table* in - * their cache slot, marked persisted so per-frame teardown leaves it - * alone. The cache slot owns that allocation; release the contained - * tables for every op_array, regardless of whether the cache buffer - * itself is heap- or arena-allocated. */ + /* VERIFY/INSTALL_GENERIC_ARGS on a CALL site (op1_type == IS_UNUSED) may + * have stashed a zend_type_arg_table* in their cache slot, marked persisted + * so per-frame teardown leaves it alone. The cache slot owns that + * allocation; release the contained tables for every op_array, regardless + * of whether the cache buffer itself is heap- or arena-allocated. + * + * (op1_type != IS_UNUSED) instead caches a resolved monomorph zend_class_entry* in slot[0]. + * That entry is owned by EG(class_table), so it mustn't be freed here. */ if (op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { char *cache_buf = (char *) ZEND_MAP_PTR_GET(op_array->run_time_cache); if (cache_buf) { @@ -874,6 +877,7 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) const zend_op *op = &op_array->opcodes[op_idx]; if ((op->opcode == ZEND_VERIFY_GENERIC_ARGUMENTS || op->opcode == ZEND_INSTALL_GENERIC_ARGS) + && op->op1_type == IS_UNUSED && op->result.num) { void **cache_slot = (void **) (cache_buf + op->result.num); zend_type_arg_table *t = (zend_type_arg_table *) cache_slot[0]; diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 61a719a2f493..bbc89c033aa6 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9108,25 +9108,7 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); - /* Monomorphize: synthesize (or look up) Box and swap both the - * object's class entry and the pending constructor call. The monomorph - * shares Box's property layout, so swapping ce is safe; swapping - * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } if (UNEXPECTED(EG(exception))) { @@ -9180,19 +9162,7 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } if (UNEXPECTED(EG(exception))) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 95197c71e7d9..5ea0ca515d86 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -382,8 +382,8 @@ static zend_vm_opcode_handler_func_t zend_vm_get_opcode_handler_func(uint8_t opc #else # define ZEND_OPCODE_HANDLER_ARGS zend_execute_data *execute_data, const zend_op *opline # define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU execute_data, opline -# define ZEND_OPCODE_HANDLER_ARGS_EX ZEND_OPCODE_HANDLER_ARGS, -# define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX ZEND_OPCODE_HANDLER_ARGS_PASSTHRU, +# define ZEND_OPCODE_HANDLER_ARGS_EX ZEND_OPCODE_HANDLER_ARGS, +# define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX ZEND_OPCODE_HANDLER_ARGS_PASSTHRU, #endif #if defined(ZEND_VM_FP_GLOBAL_REG) && defined(ZEND_VM_IP_GLOBAL_REG) @@ -22290,25 +22290,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); - /* Monomorphize: synthesize (or look up) Box and swap both the - * object's class entry and the pending constructor call. The monomorph - * shares Box's property layout, so swapping ce is safe; swapping - * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } if (UNEXPECTED(EG(exception))) { @@ -22362,19 +22344,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } if (UNEXPECTED(EG(exception))) { @@ -37925,25 +37895,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); - /* Monomorphize: synthesize (or look up) Box and swap both the - * object's class entry and the pending constructor call. The monomorph - * shares Box's property layout, so swapping ce is safe; swapping - * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } if (UNEXPECTED(EG(exception))) { @@ -37997,19 +37949,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } if (UNEXPECTED(EG(exception))) { @@ -75584,25 +75524,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); - /* Monomorphize: synthesize (or look up) Box and swap both the - * object's class entry and the pending constructor call. The monomorph - * shares Box's property layout, so swapping ce is safe; swapping - * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } if (UNEXPECTED(EG(exception))) { @@ -75656,19 +75578,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } if (UNEXPECTED(EG(exception))) { @@ -91219,25 +91129,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); - /* Monomorphize: synthesize (or look up) Box and swap both the - * object's class entry and the pending constructor call. The monomorph - * shares Box's property layout, so swapping ce is safe; swapping - * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } if (UNEXPECTED(EG(exception))) { @@ -91291,19 +91183,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } if (UNEXPECTED(EG(exception))) { From 754368edbc75f38699520a6073e8e9dccadfa9e2 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 15 Jun 2026 22:41:44 +0700 Subject: [PATCH 15/30] Cache non-turbofish tables, turbofish lookups, concrete monomorphs --- Zend/Optimizer/compact_literals.c | 15 +- Zend/zend_compile.c | 342 +++++++++++++++++++++--------- Zend/zend_compile.h | 36 ++++ Zend/zend_opcode.c | 9 +- Zend/zend_operators.h | 11 +- Zend/zend_vm_def.h | 14 +- Zend/zend_vm_execute.h | 60 ++++-- 7 files changed, 353 insertions(+), 134 deletions(-) diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c index 04345bd1c7ea..e0d50166a7d5 100644 --- a/Zend/Optimizer/compact_literals.c +++ b/Zend/Optimizer/compact_literals.c @@ -696,14 +696,15 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx break; case ZEND_VERIFY_GENERIC_ARGUMENTS: case ZEND_INSTALL_GENERIC_ARGS: - /* When this site has a turbofish (args_id in extended_value), - * the compiler allocated a 2-slot inline cache for the - * (zend_type_arg_table*, cache key) pair — re-allocate it - * here so the offset stays in sync with compact_literals' - * fresh cache_size. */ - if (opline->extended_value != 0) { + /* When this site has a turbofish (args_id in extended_value) + * or is a call (op1_type == IS_UNUSED, which caches its table + * regardless), the compiler allocated a 3-slot inline cache for + * the (zend_type_arg_table*, cache key, turbofish entry) — re- + * allocate it here so the offset stays in sync with + * compact_literals' fresh cache_size. */ + if (opline->extended_value != 0 || opline->op1_type == IS_UNUSED) { opline->result.num = cache_size; - cache_size += 2 * sizeof(void *); + cache_size += 3 * sizeof(void *); } break; case ZEND_CATCH: diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index f4be8859e305..598d241a733d 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -734,11 +734,65 @@ static bool zend_call_is_cacheable_against_args( * executing op_array — slot[0] = cached zend_type_arg_table*, slot[1] = * cache key. Runtime cache memory is allocated per-process and writable * even when the op_array's persistent metadata lives in opcache SHM. */ +/* Cache key for a generic call made WITHOUT turbofish (args_box == NULL). The + * resolved type-arg table is then a function only of the callee's declared + * defaults (plus bound fall-backs for unset slots) — provided no slot is + * filled by value-directed inference and no default itself references an outer + * type parameter (whose binding would make the table caller-dependent and, via + * the borrowed caller-frame type_ref, unsafe to persist). When both hold the + * table is invariant across every call at this site, so a constant key turns + * the per-call-site cache slot into a build-once memo. Returns 0 ("uncacheable") + * otherwise, falling back to the original rebuild-every-call behaviour. */ +static uintptr_t zend_compute_call_default_cache_key(const zend_function *fbc) +{ + if (!ZEND_USER_CODE(fbc->common.type)) { + return 0; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params) { + return 0; + } + for (uint32_t i = 0; i < params->count; i++) { + const zend_type *def = ¶ms->parameters[i].default_type; + if (ZEND_TYPE_IS_SET(*def) && ZEND_TYPE_HAS_TYPE_PARAMETER(*def)) { + /* default is `= T` of an outer param -> caller-dependent. */ + return 0; + } + } + return ZEND_TURBOFISH_CACHE_KEY_CONCRETE; +} + ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( zend_execute_data *call, const zend_type *args_box, void **cache_slot) { + /* Concrete call site (no type-parameter refs in the turbofish, or an + * all-default non-turbofish call): the resolved table is invariant across + * every call here, recorded by the CONCRETE sentinel in slot[1]. Once it is + * cached, return it directly — skip recomputing the binding fingerprint key + * (which would only re-derive the same CONCRETE sentinel). */ + if (cache_slot && cache_slot[0] + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + return (zend_type_arg_table *) cache_slot[0]; + } if (!args_box) { - return zend_build_generic_call_type_args(call, NULL); + /* Non-turbofish generic call. The table is built from the callee's + * defaults (and any value inference). When inference won't fire and no + * default is caller-dependent, the result is invariant at this site — + * cache it the same way the turbofish path does so repeated calls (e.g. + * an internal `add_node($g, $x)`) stop rebuilding+freeing a table whose + * contents never change. */ + uintptr_t nkey = (cache_slot && zend_call_is_cacheable_against_args(call->func, NULL)) + ? zend_compute_call_default_cache_key(call->func) : 0; + if (nkey && cache_slot[0] && (uintptr_t)cache_slot[1] == nkey) { + return (zend_type_arg_table *)cache_slot[0]; + } + zend_type_arg_table *nt = zend_build_generic_call_type_args(call, NULL); + if (nt && nkey && !cache_slot[0]) { + cache_slot[0] = nt; + cache_slot[1] = (void *)nkey; + nt->persisted = true; + } + return nt; } zend_execute_data *caller = EG(current_execute_data); uintptr_t key = zend_compute_call_cache_key(args_box, caller); @@ -773,10 +827,12 @@ static uintptr_t zend_compute_new_mono_cache_key( const zend_execute_data *ex) { uintptr_t key = (uintptr_t) base * 0x9E3779B97F4A7C15ULL; + bool has_tref = false; for (uint32_t i = 0; i < nwa->count; i++) { if (!ZEND_TYPE_HAS_TYPE_PARAMETER(nwa->args[i])) { continue; } + has_tref = true; const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(nwa->args[i]); if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { if (!ex || !ex->type_args || ref->index >= ex->type_args->count) { @@ -795,6 +851,13 @@ static uintptr_t zend_compute_new_mono_cache_key( key = key * 0x100000001B3ULL + (uintptr_t) cs + i + 1; } } + /* No type-parameter refs: the monomorph is frame-independent. Return the + * CONCRETE sentinel so the caller can take a key-free fast path (validating + * only that the cached monomorph's base matches, which guards a dynamic + * `new $cls::<...>` whose resolved base varies between calls). */ + if (!has_tref) { + return ZEND_TURBOFISH_CACHE_KEY_CONCRETE; + } if (key == 0) { key = (uintptr_t) -1; } @@ -812,15 +875,7 @@ static zend_always_inline void zend_generic_new_swap_ce( } } -/* - * Monomorphize: synthesize (or look up) Box and swap both the - * object's class entry and the pending constructor call. The monomorph - * shares Box's property layout, so swapping ce is safe; swapping - * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. - * Use a call-site inline cache to avoid building the canonical name and performing the class-table loookup again. - * - * `new C::<...>` resolution with a call-site monomorph cache. Without the cache +/* `new C::<...>` resolution with a call-site monomorph cache. Without the cache * every instantiation rebuilds the canonical class name (smart_str + interning), * lowercases it, and hashes EG(class_table) — all to rediscover the same * monomorph. On a cache hit we skip the arity/bound check and the synthesis @@ -842,8 +897,22 @@ ZEND_API void zend_apply_generic_new( ? ZEND_TYPE_NAMED_WITH_ARGS(*args_box) : NULL; if (nwa && ce->generic_parameters && cache_slot) { + /* Concrete-args fast path: the monomorph is frame-independent, so a + * cached one whose base matches reuses directly — no key recomputation. + * The base check (`->parent == ce`: a monomorph's parent is its base) + * keeps a dynamic `new $cls::<...>` correct when $cls varies. */ + if (cache_slot[0] + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE + && ((zend_class_entry *) cache_slot[0])->parent == ce) { + zend_generic_new_swap_ce(new_obj, call, ce, (zend_class_entry *) cache_slot[0]); + return; + } uintptr_t key = zend_compute_new_mono_cache_key(ce, nwa, EG(current_execute_data)); - if (key && cache_slot[0] && (uintptr_t) cache_slot[1] == key) { + /* CONCRETE keys are handled by the fast path above; reusing here without + * the base check would mis-serve a dynamic base, so require a real + * (binding-fingerprint) key for the generic hit. */ + if (key && key != ZEND_TURBOFISH_CACHE_KEY_CONCRETE + && cache_slot[0] && (uintptr_t) cache_slot[1] == key) { zend_generic_new_swap_ce(new_obj, call, ce, (zend_class_entry *) cache_slot[0]); return; } @@ -1062,6 +1131,125 @@ static bool zend_check_pre_erasure_type_value( * and sets EG(exception) on the first failure. Only bare T at the top level is * substituted in this pass — composite shapes (array, Box, ?T) fall back * to the erased bound check at RECV. */ +/* Verify one value parameter whose pre-erasure type is a direct FUNCTION_LIKE + * T-ref (parameter position `arg_idx`, type-parameter index `tp_index`) against + * the per-call binding. Shared by both the plan-driven fast path and the hash + * fallback so the substitution / variadic-sweep / error semantics stay + * identical. Returns false (with EG(exception) set) on the first violation. */ +static zend_always_inline bool zend_verify_one_generic_param( + zend_execute_data *call, const zend_function *fbc, + const zend_type_named_with_args *nwa, + uint32_t arg_idx, uint32_t tp_index, uint32_t num_args, bool strict) +{ + zend_type substituted; + if (nwa) { + if (tp_index >= nwa->count) return true; + substituted = nwa->args[tp_index]; + } else { + /* No turbofish args_box for this call — resolve directly from the + * captured T-table (e.g., a closure with no turbofish at its own + * call site, whose body references the outer frame's T). */ + if (tp_index >= call->type_args->count) return true; + const zend_type *resolved = zend_type_arg_entry_type( + &call->type_args->entries[tp_index]); + if (!resolved) return true; + substituted = *resolved; + } + if (!ZEND_TYPE_IS_SET(substituted)) return true; + + /* If the turbofish arg is itself a forwarded TYPE_PARAMETER (e.g. + * `id::($x)` inside `nested`), the caller's binding has already been + * resolved into call->type_args by zend_build_generic_call_type_args. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(substituted)) { + if (!call->type_args || tp_index >= call->type_args->count) { + return true; + } + const zend_type *resolved = zend_type_arg_entry_type( + &call->type_args->entries[tp_index]); + if (!resolved) { + return true; + } + substituted = *resolved; + } + + /* When the pre-erasure parameter slot is variadic (T ...$xs), the single + * key covers every value supplied at runtime. Sweep from arg_idx through + * num_args-1 so each variadic element gets the same reified T-check. */ + bool is_variadic_slot = (fbc->common.fn_flags & ZEND_ACC_VARIADIC) + && arg_idx == fbc->common.num_args; + uint32_t sweep_end = is_variadic_slot ? num_args : arg_idx + 1; + + for (uint32_t aidx = arg_idx; aidx < sweep_end; aidx++) { + zval *arg = ZEND_CALL_ARG(call, aidx + 1); + zval *target = arg; + const zend_reference *zref = NULL; + if (Z_ISREF_P(target)) { + zref = Z_REF_P(target); + target = Z_REFVAL_P(target); + } + + /* Pre-erasure shapes don't match the canonical PHP type layout + * (scalars sit as CODE-only list members instead of bits on the + * outer mask), so walk them directly. */ + bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); + if (!ok) { + zend_string *expected = zend_type_to_string(substituted); + const zend_arg_info *ai; + if (is_variadic_slot) { + ai = &fbc->common.arg_info[fbc->common.num_args]; + } else { + ai = (aidx < fbc->common.num_args) + ? &fbc->common.arg_info[aidx] : NULL; + } + zend_throw_error(zend_ce_type_error, + "%s%s%s(): Argument #%u%s%s%s must be of type %s, %s given", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + aidx + 1, + (ai && ai->name) ? " ($" : "", + (ai && ai->name) ? ZSTR_VAL(ai->name) : "", + (ai && ai->name) ? ")" : "", + ZSTR_VAL(expected), zend_zval_value_name(target)); + zend_string_release(expected); + return false; + } + } + return true; +} + +/* Build the packed value-check plan from a callee's `parameters` table: one + * entry per direct FUNCTION_LIKE T-ref value parameter, in hash order (so the + * "first failing argument" diagnostic matches the legacy iteration). */ +static zend_generic_value_check_plan *zend_build_generic_value_check_plan( + const zend_op_array *op_array) +{ + const HashTable *pre = op_array->generic_types->parameters; + uint32_t cnt = 0; + zend_ulong arg_idx; + zend_type *pe_type_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + (void) arg_idx; + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + if (ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr)->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + cnt++; + } ZEND_HASH_FOREACH_END(); + + zend_generic_value_check_plan *plan = emalloc( + offsetof(zend_generic_value_check_plan, checks) + cnt * sizeof(zend_generic_value_check)); + plan->count = cnt; + uint32_t w = 0; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + plan->checks[w].arg_idx = (uint32_t) arg_idx; + plan->checks[w].tp_index = ref->index; + w++; + } ZEND_HASH_FOREACH_END(); + return plan; +} + ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box) { const zend_type_named_with_args *nwa = NULL; @@ -1079,8 +1267,35 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ || !fbc->op_array.generic_types->parameters) { return true; } - const HashTable *pre = fbc->op_array.generic_types->parameters; + uint32_t num_args = ZEND_CALL_NUM_ARGS(call); + /* Loop-invariant: strictness is a property of the calling frame. */ + bool strict = EG(current_execute_data) + && EG(current_execute_data)->func + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); + + zend_generic_type_table *gt = fbc->op_array.generic_types; + zend_generic_value_check_plan *plan = gt->value_check_plan; + if (!plan && !gt->persisted) { + plan = zend_build_generic_value_check_plan(&fbc->op_array); + gt->value_check_plan = plan; + } + + if (plan) { + for (uint32_t p = 0; p < plan->count; p++) { + uint32_t arg_idx = plan->checks[p].arg_idx; + if (arg_idx >= num_args) continue; + if (!zend_verify_one_generic_param(call, fbc, nwa, + arg_idx, plan->checks[p].tp_index, num_args, strict)) { + return false; + } + } + return true; + } + + /* Fallback for opcache-persisted tables (read-only SHM, no plan cached): + * iterate the parameters hash directly. */ + const HashTable *pre = gt->parameters; zend_ulong arg_idx; zend_type *pe_type_ptr; ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { @@ -1088,89 +1303,9 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; - zend_type substituted; - if (nwa) { - if (ref->index >= nwa->count) continue; - substituted = nwa->args[ref->index]; - } else { - /* No turbofish args_box for this call — resolve directly from the - * captured T-table (e.g., a closure with no turbofish at its own - * call site, whose body references the outer frame's T). */ - if (ref->index >= call->type_args->count) continue; - const zend_type *resolved = zend_type_arg_entry_type( - &call->type_args->entries[ref->index]); - if (!resolved) continue; - substituted = *resolved; - } - if (!ZEND_TYPE_IS_SET(substituted)) continue; - - /* If the turbofish arg is itself a forwarded TYPE_PARAMETER (e.g. - * `id::($x)` inside `nested`), the caller's binding has - * already been resolved into call->type_args by - * zend_build_generic_call_type_args. Substitute the resolved - * zend_type in so downstream class/scalar/composite checks run - * against the actual bound type rather than the literal T-ref. */ - if (ZEND_TYPE_HAS_TYPE_PARAMETER(substituted)) { - if (!call->type_args || ref->index >= call->type_args->count) { - continue; - } - const zend_type *resolved = zend_type_arg_entry_type( - &call->type_args->entries[ref->index]); - if (!resolved) { - /* No binding available; the erased parameter type (the - * parameter's bound) already accepted the value. */ - continue; - } - substituted = *resolved; - } - - bool strict = EG(current_execute_data) - && EG(current_execute_data)->func - && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); - - /* When the pre-erasure parameter slot is variadic (T ...$xs), the - * single key in `pre` covers every value supplied at runtime. Sweep - * from arg_idx through num_args-1 so each variadic element gets the - * same reified T-check, not just the first. */ - bool is_variadic_slot = (fbc->common.fn_flags & ZEND_ACC_VARIADIC) - && arg_idx == fbc->common.num_args; - uint32_t sweep_end = is_variadic_slot ? num_args : (uint32_t) arg_idx + 1; - - for (uint32_t aidx = (uint32_t) arg_idx; aidx < sweep_end; aidx++) { - zval *arg = ZEND_CALL_ARG(call, aidx + 1); - zval *target = arg; - const zend_reference *zref = NULL; - if (Z_ISREF_P(target)) { - zref = Z_REF_P(target); - target = Z_REFVAL_P(target); - } - - /* Pre-erasure shapes don't match the canonical PHP type layout - * (scalars sit as CODE-only list members instead of bits on the - * outer mask), so walk them directly. */ - bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); - if (!ok) { - zend_string *expected = zend_type_to_string(substituted); - const zend_arg_info *ai; - if (is_variadic_slot) { - ai = &fbc->common.arg_info[fbc->common.num_args]; - } else { - ai = (aidx < fbc->common.num_args) - ? &fbc->common.arg_info[aidx] : NULL; - } - zend_throw_error(zend_ce_type_error, - "%s%s%s(): Argument #%u%s%s%s must be of type %s, %s given", - fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", - fbc->common.scope ? "::" : "", - fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", - aidx + 1, - (ai && ai->name) ? " ($" : "", - (ai && ai->name) ? ZSTR_VAL(ai->name) : "", - (ai && ai->name) ? ")" : "", - ZSTR_VAL(expected), zend_zval_value_name(target)); - zend_string_release(expected); - return false; - } + if (!zend_verify_one_generic_param(call, fbc, nwa, + (uint32_t) arg_idx, ref->index, num_args, strict)) { + return false; } } ZEND_HASH_FOREACH_END(); return true; @@ -1443,13 +1578,16 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t opline->op1.num = 0; } opline->result_type = IS_UNUSED; - /* When there's a turbofish, reserve a 2-slot inline cache in the caller - * op_array's runtime cache: slot[0] = cached zend_type_arg_table*, - * slot[1] = caller-binding fingerprint (cache key). The runtime cache is - * per-process and writable even when the op_array's side tables are - * persisted to opcache SHM, so this cache survives opcache without the - * SHM-write gating that previously disabled it. */ - opline->result.num = args_id ? zend_alloc_cache_slots(2) : 0; + /* Reserve a 3-slot inline cache in the caller op_array's runtime cache: + * slot[0] = cached zend_type_arg_table* (CALL) / monomorph ce* (NEW), + * slot[1] = cache key (caller-binding fingerprint), + * slot[2] = cached zend_turbofish_args_entry* (resolved once per site so the + * handler skips the per-call turbofish_args hash lookup). + * The runtime cache is per-process and writable even when the op_array's + * side tables are persisted to opcache SHM, so this cache survives opcache + * without the SHM-write gating that previously disabled it. */ + opline->result.num = (args_id || kind == ZEND_VERIFY_ARITY_KIND_CALL) + ? zend_alloc_cache_slots(3) : 0; } static zend_generic_parameter_list *zend_compile_generic_type_parameter_list( diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index a39face5fa58..96698da988cd 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -155,6 +155,22 @@ typedef struct _zend_generic_parameter_list { #define ZEND_GENERIC_PARAMETER_LIST_SIZE(count) \ (sizeof(zend_generic_parameter_list) + ((count) - 1) * sizeof(zend_generic_parameter)) +/* Packed, callee-static list of the direct FUNCTION_LIKE type-parameter value + * parameters that zend_verify_generic_arg_types must runtime-check. Derived once + * from `parameters`, it replaces a per-call HashTable iteration (+ per-entry + * TYPE_PARAMETER/origin filtering) with a tight array walk. Built lazily on the + * first verify; left NULL on opcache-persisted tables (which are read-only), in + * which case verify falls back to iterating `parameters` directly. */ +typedef struct _zend_generic_value_check { + uint32_t arg_idx; /* parameter position (0-based) */ + uint32_t tp_index; /* type-parameter index to substitute (ref->index) */ +} zend_generic_value_check; + +typedef struct _zend_generic_value_check_plan { + uint32_t count; + zend_generic_value_check checks[1]; /* flexible array */ +} zend_generic_value_check_plan; + typedef struct _zend_generic_type_table { zend_type *return_type; /* function/method return; NULL if equal to erased */ zend_type *extends; /* class extends; NULL if equal */ @@ -165,6 +181,7 @@ typedef struct _zend_generic_type_table { HashTable *trait_uses; /* trait-use index -> zend_type * */ HashTable *turbofish_args; /* opline->extended_value -> zend_type * (NAMED_WITH_ARGS holding the call-site type arguments); index is stable across optimizer reorderings */ bool persisted; /* set by opcache when the table lives in SHM/file-cache memory; suppresses destruction */ + zend_generic_value_check_plan *value_check_plan; /* lazily built; request-local; NULL on persisted tables */ } zend_generic_type_table; /* Compile-time linked stack of in-scope generic type parameters. */ @@ -255,6 +272,25 @@ typedef struct _zend_turbofish_args_entry { zend_type args_box; } zend_turbofish_args_entry; +/* Resolve a generic call site's turbofish args_box, memoizing the (static per + * site) turbofish_args lookup in cache_slot[2] so the VERIFY/INSTALL handlers + * skip the hash lookup after the first call. Returns NULL when the site has no + * turbofish (args_id == 0 / no entry). */ +static zend_always_inline const zend_type *zend_generic_get_or_cache_args_box( + const zend_op_array *op_array, uint32_t args_id, void **cache_slot) +{ + zend_turbofish_args_entry *entry; + if (cache_slot && cache_slot[2]) { + entry = (zend_turbofish_args_entry *) cache_slot[2]; + } else { + entry = zend_generic_get_turbofish_call_entry(op_array, args_id); + if (cache_slot && entry) { + cache_slot[2] = entry; + } + } + return entry ? &entry->args_box : NULL; +} + /* Cache key sentinel for concrete-arg call sites (no T-refs in the args). * Cache key 0 means empty; CONCRETE means "args fully resolved at compile * time, table is invariant across calls." */ diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index bc64dbd1975a..5237caebc872 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -234,6 +234,9 @@ ZEND_API void zend_generic_type_table_destroy(zend_generic_type_table *table) { zend_hash_destroy(table->turbofish_args); FREE_HASHTABLE(table->turbofish_args); } + if (table->value_check_plan) { + efree(table->value_check_plan); + } efree(table); } @@ -868,8 +871,10 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) * allocation; release the contained tables for every op_array, regardless * of whether the cache buffer itself is heap- or arena-allocated. * - * (op1_type != IS_UNUSED) instead caches a resolved monomorph zend_class_entry* in slot[0]. - * That entry is owned by EG(class_table), so it mustn't be freed here. */ + * The NEW form of the same opcodes (op1_type != IS_UNUSED) instead caches a + * resolved monomorph zend_class_entry* in slot[0]. That entry is owned by + * EG(class_table), NOT by the cache slot, so it must be left untouched here + * — freeing it as a type_arg_table corrupts the heap. */ if (op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { char *cache_buf = (char *) ZEND_MAP_PTR_GET(op_array->run_time_cache); if (cache_buf) { diff --git a/Zend/zend_operators.h b/Zend/zend_operators.h index 27aa4fdb0486..f25b6236e773 100644 --- a/Zend/zend_operators.h +++ b/Zend/zend_operators.h @@ -80,7 +80,16 @@ ZEND_API bool ZEND_FASTCALL instanceof_function_slow(const zend_class_entry *ins static zend_always_inline bool instanceof_function( const zend_class_entry *instance_ce, const zend_class_entry *ce) { - return instance_ce == ce || instanceof_function_slow(instance_ce, ce); + /* `instance_ce->parent == ce` is a cheap second-level fast path for the + * direct-subclass case: it always implies `instance_ce instanceof ce` + * (parent is a class, never an interface). It is the common shape for a + * generic monomorph checked against its erased base (e.g. a + * `DirectedGraph` value flowing into a `DirectedGraph`-typed + * parameter), which would otherwise pay a full slow-path call on every + * boundary check. */ + return instance_ce == ce + || instance_ce->parent == ce + || instanceof_function_slow(instance_ce, ce); } ZEND_API bool zend_string_only_has_ascii_alphanumeric(const zend_string *str); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index bbc89c033aa6..ad7d67f495d6 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9078,9 +9078,8 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -9108,6 +9107,12 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) } } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } @@ -9145,9 +9150,8 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -9162,6 +9166,8 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 5ea0ca515d86..91304be18a3a 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -382,8 +382,8 @@ static zend_vm_opcode_handler_func_t zend_vm_get_opcode_handler_func(uint8_t opc #else # define ZEND_OPCODE_HANDLER_ARGS zend_execute_data *execute_data, const zend_op *opline # define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU execute_data, opline -# define ZEND_OPCODE_HANDLER_ARGS_EX ZEND_OPCODE_HANDLER_ARGS, -# define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX ZEND_OPCODE_HANDLER_ARGS_PASSTHRU, +# define ZEND_OPCODE_HANDLER_ARGS_EX ZEND_OPCODE_HANDLER_ARGS, +# define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX ZEND_OPCODE_HANDLER_ARGS_PASSTHRU, #endif #if defined(ZEND_VM_FP_GLOBAL_REG) && defined(ZEND_VM_IP_GLOBAL_REG) @@ -22260,9 +22260,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -22290,6 +22289,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } @@ -22327,9 +22332,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -22344,6 +22348,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } @@ -37865,9 +37871,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -37895,6 +37900,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI } } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } @@ -37932,9 +37943,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -37949,6 +37959,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } @@ -75494,9 +75506,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -75524,6 +75535,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } @@ -75561,9 +75578,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -75578,6 +75594,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } @@ -91099,9 +91117,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -91129,6 +91146,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG } } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Monomorphize: synthesize (or look up) Box and swap both the + * object's class entry and the pending constructor call. The monomorph + * shares Box's property layout, so swapping ce is safe; swapping + * call->func ensures the constructor's RECV opcodes verify against the + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } @@ -91166,9 +91189,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); @@ -91183,6 +91205,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } From 78b0d3d31d6e06b4f1e51a5abd3db9de97bb97b1 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 16 Jun 2026 09:01:35 +0000 Subject: [PATCH 16/30] Monomorphize generic function/method calls (reified generics) Specialize concrete turbofish calls at compile time: Graph\dfs::() compiles to a direct by-name call to the mangled monomorph and drops the per-call ZEND_VERIFY_GENERIC_ARGUMENTS opcode, making it byte-identical to a non-generic call. Monomorphs are synthesized and registered on first reference; the runtime verify/bind path remains as the fallback for non-concrete / inference-only call sites. Also: persist the generic value-check plan into opcache SHM, and gate destroy_op_array's monomorph-table scan on ZEND_ACC2_HAS_GENERIC_CALL_OPS so non-generic code is not taxed. PSL\Graph bench (aarch64, opcache, two-point Ir/iter): reified-vs-erased overhead 6.51%->4.14% (all) / 5.72%->4.19% (query); wall 9.04%->5.89% (no-JIT). Erased baseline unchanged; generics tax -37%. Digests identical across no-JIT/tracing/-n, generics .phpt 0 new regressions, valgrind-clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- Zend/Optimizer/compact_literals.c | 7 +- .../generics/reification/inference_basic.phpt | 4 +- .../reification/variadic_t_reified.phpt | 8 +- Zend/zend_compile.c | 373 ++++++++- Zend/zend_compile.h | 37 + Zend/zend_execute.c | 13 + Zend/zend_execute.h | 5 + Zend/zend_inheritance.c | 277 ++++++- Zend/zend_inheritance.h | 10 + Zend/zend_opcode.c | 22 +- Zend/zend_vm_def.h | 193 ++++- Zend/zend_vm_execute.h | 712 +++++++++++++++--- ext/opcache/zend_file_cache.c | 7 + ext/opcache/zend_persist.c | 56 ++ ext/opcache/zend_persist_calc.c | 43 +- 15 files changed, 1614 insertions(+), 153 deletions(-) diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c index e0d50166a7d5..49bcf7b13196 100644 --- a/Zend/Optimizer/compact_literals.c +++ b/Zend/Optimizer/compact_literals.c @@ -698,13 +698,12 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx case ZEND_INSTALL_GENERIC_ARGS: /* When this site has a turbofish (args_id in extended_value) * or is a call (op1_type == IS_UNUSED, which caches its table - * regardless), the compiler allocated a 3-slot inline cache for - * the (zend_type_arg_table*, cache key, turbofish entry) — re- - * allocate it here so the offset stays in sync with + * regardless), the compiler allocated a 5-slot inline cache — + * re-allocate it here so the offset stays in sync with * compact_literals' fresh cache_size. */ if (opline->extended_value != 0 || opline->op1_type == IS_UNUSED) { opline->result.num = cache_size; - cache_size += 3 * sizeof(void *); + cache_size += 5 * sizeof(void *); } break; case ZEND_CATCH: diff --git a/Zend/tests/generics/reification/inference_basic.phpt b/Zend/tests/generics/reification/inference_basic.phpt index 240d9ed4da20..9c05c291b024 100644 --- a/Zend/tests/generics/reification/inference_basic.phpt +++ b/Zend/tests/generics/reification/inference_basic.phpt @@ -18,7 +18,7 @@ try { echo "TypeError: ", $e->getMessage(), "\n"; } ?> ---EXPECT-- +--EXPECTF-- Foo Bar -TypeError: kind(): Argument #1 ($x) must be of type Foo, Bar given +TypeError: kind(): Argument #1 ($x) must be of type Foo, Bar given, called in %s on line %d diff --git a/Zend/tests/generics/reification/variadic_t_reified.phpt b/Zend/tests/generics/reification/variadic_t_reified.phpt index 87b01a3001d0..9cdd17bcf6fb 100644 --- a/Zend/tests/generics/reification/variadic_t_reified.phpt +++ b/Zend/tests/generics/reification/variadic_t_reified.phpt @@ -67,9 +67,9 @@ try { ?> --EXPECTF-- int(6) -1: sum(): Argument #2 ($xs) must be of type int, string given -2: sum(): Argument #3 ($xs) must be of type int, string given +1: sum(): Argument #2 must be of type int, string given, called in %s on line %d +2: sum(): Argument #3 must be of type int, string given, called in %s on line %d string(6) ":: 123" -3: concat(): Argument #3 ($xs) must be of type int, array given +3: concat(): Argument #3 must be of type int, array given, called in %s on line %d int(3) -4: herd(): Argument #2 ($xs) must be of type Dog, Cat given +4: herd(): Argument #2 must be of type Dog, Cat given, called in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 598d241a733d..680da3c5ee88 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -809,6 +809,109 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( return t; } +/* Resolve (and cache per call site) the concrete monomorph for a turbofish CALL + * with concrete args in `args_box`; *out_type_args gets the invariant type-arg + * table for body T-ref resolution. NULL when the site can't be monomorphized. */ +static zend_type_arg_table *zend_build_concrete_call_type_args( + const zend_function *fbc, const zend_type *args_box); + +ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( + zend_execute_data *call, const zend_type *args_box, uint32_t arity, void **cache_slot, + zend_type_arg_table **out_type_args) +{ + zend_function *base = call->func; + + if (cache_slot + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH + && cache_slot[3] == (void *) base) { + *out_type_args = (zend_type_arg_table *) cache_slot[4]; + return (zend_function *) cache_slot[0]; + } + + if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return NULL; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + + /* Enforce arity + bounds as the erased VERIFY path does (first call only). */ + zend_check_generic_call_arguments(base, arity, args_box); + if (UNEXPECTED(EG(exception))) { + return NULL; + } + + zend_function *mono = zend_synthesize_function_monomorph(base, nwa->args, nwa->count); + if (!mono) { + return NULL; + } + + zend_type_arg_table *table = zend_build_concrete_call_type_args(base, args_box); + if (table) { + table->persisted = true; + } + + if (cache_slot) { + cache_slot[0] = mono; + cache_slot[1] = (void *) ZEND_TURBOFISH_CACHE_KEY_MONOMORPH; + cache_slot[3] = (void *) base; + cache_slot[4] = table; + } + *out_type_args = table; + return mono; +} + +/* Monomorphize a non-turbofish generic call once its per-frame type-arg table + * `resolved` is known concrete and invariant, swapping call->func to a plain + * monomorph to skip the per-call value check. NULL keeps the erased path. */ +ZEND_API zend_function *zend_try_monomorph_resolved_call( + zend_execute_data *call, zend_type_arg_table *resolved, void **cache_slot, + zend_type_arg_table **out_type_args) +{ + zend_function *base = call->func; + + if (cache_slot + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH + && cache_slot[3] == (void *) base) { + *out_type_args = (zend_type_arg_table *) cache_slot[4]; + return (zend_function *) cache_slot[0]; + } + + if (!resolved || resolved->count == 0 + || !ZEND_USER_CODE(base->type) + || !base->op_array.generic_parameters) { + return NULL; + } + + /* All bindings must be concrete (free of T-refs). */ + ALLOCA_FLAG(use_heap) + zend_type *args = do_alloca(sizeof(zend_type) * resolved->count, use_heap); + bool ok = true; + for (uint32_t i = 0; i < resolved->count; i++) { + const zend_type *bt = zend_type_arg_entry_type(&resolved->entries[i]); + if (!bt || !ZEND_TYPE_IS_SET(*bt) || zend_type_contains_type_parameter(*bt)) { + ok = false; + break; + } + args[i] = *bt; + } + + zend_function *mono = ok + ? zend_synthesize_function_monomorph(base, args, resolved->count) + : NULL; + free_alloca(args, use_heap); + if (!mono) { + return NULL; + } + + if (cache_slot) { + cache_slot[0] = mono; + cache_slot[1] = (void *) ZEND_TURBOFISH_CACHE_KEY_MONOMORPH; + cache_slot[3] = (void *) base; + cache_slot[4] = resolved; + } + *out_type_args = resolved; + return mono; +} + ZEND_API zend_class_entry *zend_get_called_scope(const zend_execute_data *ex); /* Cache key for a `new C::<...>` site's resolved monomorph. The monomorph is a @@ -1218,28 +1321,32 @@ static zend_always_inline bool zend_verify_one_generic_param( return true; } -/* Build the packed value-check plan from a callee's `parameters` table: one - * entry per direct FUNCTION_LIKE T-ref value parameter, in hash order (so the - * "first failing argument" diagnostic matches the legacy iteration). */ -static zend_generic_value_check_plan *zend_build_generic_value_check_plan( - const zend_op_array *op_array) +/* Count the direct FUNCTION_LIKE T-ref value parameters; the persister and the + * runtime builder share it so both size the plan identically. */ +ZEND_API uint32_t zend_count_generic_value_checks(const HashTable *parameters) { - const HashTable *pre = op_array->generic_types->parameters; uint32_t cnt = 0; zend_ulong arg_idx; zend_type *pe_type_ptr; - ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + ZEND_HASH_FOREACH_NUM_KEY_PTR(parameters, arg_idx, pe_type_ptr) { (void) arg_idx; if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; if (ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr)->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; cnt++; } ZEND_HASH_FOREACH_END(); + return cnt; +} - zend_generic_value_check_plan *plan = emalloc( - offsetof(zend_generic_value_check_plan, checks) + cnt * sizeof(zend_generic_value_check)); - plan->count = cnt; +/* Fill a plan buffer (sized for zend_count_generic_value_checks() entries): one + * entry per direct FUNCTION_LIKE T-ref value parameter, in hash order so the + * "first failing argument" diagnostic matches the legacy iteration. */ +ZEND_API void zend_fill_generic_value_check_plan( + zend_generic_value_check_plan *plan, const HashTable *parameters) +{ + zend_ulong arg_idx; + zend_type *pe_type_ptr; uint32_t w = 0; - ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + ZEND_HASH_FOREACH_NUM_KEY_PTR(parameters, arg_idx, pe_type_ptr) { if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; @@ -1247,6 +1354,18 @@ static zend_generic_value_check_plan *zend_build_generic_value_check_plan( plan->checks[w].tp_index = ref->index; w++; } ZEND_HASH_FOREACH_END(); + plan->count = w; +} + +/* Build the packed value-check plan from a callee's `parameters` table into + * request-local memory. */ +static zend_generic_value_check_plan *zend_build_generic_value_check_plan( + const HashTable *parameters) +{ + uint32_t cnt = zend_count_generic_value_checks(parameters); + zend_generic_value_check_plan *plan = emalloc( + offsetof(zend_generic_value_check_plan, checks) + cnt * sizeof(zend_generic_value_check)); + zend_fill_generic_value_check_plan(plan, parameters); return plan; } @@ -1277,7 +1396,7 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ zend_generic_type_table *gt = fbc->op_array.generic_types; zend_generic_value_check_plan *plan = gt->value_check_plan; if (!plan && !gt->persisted) { - plan = zend_build_generic_value_check_plan(&fbc->op_array); + plan = zend_build_generic_value_check_plan(fbc->op_array.generic_types->parameters); gt->value_check_plan = plan; } @@ -1455,6 +1574,8 @@ ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint3 #define ZEND_VERIFY_ARITY_KIND_CALL 0 #define ZEND_VERIFY_ARITY_KIND_NEW 1 +/* Site dispatches directly to a monomorph by name; emit no VERIFY/INSTALL. */ +#define ZEND_VERIFY_ARITY_KIND_NONE 2 static zend_type zend_compile_turbofish_args_type(zend_ast *turbofish_ast) { @@ -1525,6 +1646,84 @@ static bool zend_can_install_call_args_statically( params, ZEND_TYPE_NAMED_WITH_ARGS(*args_box), arity); } +/* Compile-time equivalent of zend_build_generic_call_type_args for a concrete + * call site: each entry borrows its type_ref into the args_box NWA (i=passed). NULL when any arg still contains a T-ref. */ +static zend_type_arg_table *zend_build_concrete_call_type_args( + const zend_function *fbc, const zend_type *args_box) +{ + if (!fbc || !ZEND_USER_CODE(fbc->common.type)) { + return NULL; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params || params->count == 0) { + return NULL; + } + + uint32_t passed = 0; + const zend_type *passed_args = NULL; + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + passed = nwa->count; + passed_args = nwa->args; + } + + zend_type_arg_table *table = zend_type_arg_table_alloc(params->count); + for (uint32_t i = 0; i < params->count; i++) { + const zend_type *src = NULL; + if (i < passed) { + src = &passed_args[i]; + } else if (ZEND_TYPE_IS_SET(params->parameters[i].default_type)) { + src = ¶ms->parameters[i].default_type; + } + if (!src || !ZEND_TYPE_IS_SET(*src)) { + continue; + } + if (zend_type_contains_type_parameter(*src)) { + zend_type_arg_table_destroy(table); + return NULL; + } + table->entries[i].name = zend_type_arg_canonical_name(*src); + table->entries[i].type_ref = src; + } + + return table; +} + +/* If this turbofish CALL site is concrete, build the resolved type-arg table now + * and stash it on the turbofish entry so persist can relocate it into SHM. */ +static bool zend_try_attach_concrete_call_table( + const zend_function *fbc, const zend_type *args_box, uint32_t args_id) +{ + if (!fbc || !args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return false; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_type_parameter(nwa->args[i])) { + return false; + } + } + if (!zend_call_is_cacheable_against_args(fbc, args_box)) { + return false; + } + zend_type_arg_table *ct = zend_build_concrete_call_type_args(fbc, args_box); + if (!ct) { + return false; + } + zend_generic_type_table *gtt = CG(active_op_array)->generic_types; + zend_turbofish_args_entry *entry = + zend_hash_index_find_ptr(gtt->turbofish_args, args_id); + ZEND_ASSERT(entry != NULL); + entry->concrete_table = ct; + entry->concrete_skip_value_check = + fbc->op_array.generic_types + && fbc->op_array.generic_types->parameters + && zend_count_generic_value_checks( + fbc->op_array.generic_types->parameters) == 0; + return true; +} + static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc) { uint32_t arity = 0; @@ -1562,6 +1761,11 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t if (kind == ZEND_VERIFY_ARITY_KIND_CALL && zend_can_install_call_args_statically(fbc, args_box, arity)) { opcode = ZEND_INSTALL_GENERIC_ARGS; + zend_try_attach_concrete_call_table(fbc, args_box, args_id); + } else if (kind == ZEND_VERIFY_ARITY_KIND_CALL && turbofish_ast && args_id) { + /* Concrete args but VERIFY stays (bounds not statically decidable); + * still attach the invariant table for the handler to install. */ + zend_try_attach_concrete_call_table(fbc, args_box, args_id); } zend_op *opline = get_next_op(); @@ -1578,16 +1782,22 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t opline->op1.num = 0; } opline->result_type = IS_UNUSED; - /* Reserve a 3-slot inline cache in the caller op_array's runtime cache: + /* Reserve a 5-slot inline cache in the caller op_array's runtime cache: * slot[0] = cached zend_type_arg_table* (CALL) / monomorph ce* (NEW), - * slot[1] = cache key (caller-binding fingerprint), - * slot[2] = cached zend_turbofish_args_entry* (resolved once per site so the - * handler skips the per-call turbofish_args hash lookup). + * slot[1] = cache key (caller-binding fingerprint) / state sentinel, + * slot[2] = cached zend_turbofish_args_entry* (skips the per-call lookup), + * slot[3] = promotion/monomorph memo (resolved callee guard), + * slot[4] = monomorph type-arg table. * The runtime cache is per-process and writable even when the op_array's - * side tables are persisted to opcache SHM, so this cache survives opcache - * without the SHM-write gating that previously disabled it. */ + * side tables are persisted to opcache SHM. */ opline->result.num = (args_id || kind == ZEND_VERIFY_ARITY_KIND_CALL) - ? zend_alloc_cache_slots(3) : 0; + ? zend_alloc_cache_slots(5) : 0; + + /* Mark so destroy_op_array scans opcodes for a CALL-form generic-args slot + * that may own a zend_type_arg_table; non-generic op_arrays skip the scan. */ + if (opline->op1_type == IS_UNUSED && opline->result.num) { + CG(active_op_array)->fn_flags2 |= ZEND_ACC2_HAS_GENERIC_CALL_OPS; + } } static zend_generic_parameter_list *zend_compile_generic_type_parameter_list( @@ -6760,8 +6970,111 @@ static uint32_t zend_compile_frameless_icall(znode *result, const zend_ast_list return zend_compile_frameless_icall_ex(result, args, fbc, frameless_function_info, type); } +/* For a turbofish call with only concrete args, return the lowercased mangled + * monomorph name (`base`) used as the by-name dispatch key, or NULL to + * keep the runtime VERIFY path. */ +static zend_string *zend_build_concrete_turbofish_monomorph_name( + zend_string *base_name, zend_ast *turbofish_ast) +{ + if (!turbofish_ast) { + return NULL; + } + zend_ast_list *list = zend_ast_get_list(turbofish_ast); + uint32_t arity = list->children; + if (arity == 0 || arity > ZEND_GENERIC_MAX_PARAMS) { + return NULL; + } + + /* Bail on any T-ref or class-name arg: the mangled name is lowercased and the + * synthesize-by-name path reparses it, which would lose class-name case. Only + * case-insensitive scalar args round-trip losslessly. */ + zend_type args[ZEND_GENERIC_MAX_PARAMS]; + bool ok = true; + uint32_t built = 0; + for (uint32_t i = 0; i < arity; i++) { + args[i] = zend_compile_pre_erasure_typename(list->child[i]); + built++; + if (zend_type_contains_type_parameter(args[i]) + || ZEND_TYPE_HAS_NAME(args[i]) + || ZEND_TYPE_HAS_LITERAL_NAME(args[i]) + || ZEND_TYPE_HAS_NAMED_WITH_ARGS(args[i]) + || ZEND_TYPE_HAS_LIST(args[i])) { + ok = false; + break; + } + } + + zend_string *result = NULL; + if (ok) { + zend_string *canonical = + zend_generic_canonical_class_name(base_name, args, arity); + result = zend_string_tolower(canonical); + zend_string_release(canonical); + } + + for (uint32_t i = 0; i < built; i++) { + zend_type_release(args[i], /* persistent */ false); + } + return result; +} + +/* Emit the literal triple for an INIT_NS_FCALL_BY_NAME targeting the mangled + * monomorph name (mirrors zend_add_ns_func_name_literal). -1 when not concrete. */ +static int zend_add_ns_monomorph_name_literal(zend_string *full_name, zend_ast *turbofish_ast) +{ + zend_string *mangled_lc_full = + zend_build_concrete_turbofish_monomorph_name(full_name, turbofish_ast); + if (!mangled_lc_full) { + return -1; + } + + /* slot[0]: original-case mangled full name (only used in diagnostics). */ + zend_string *orig = zend_string_dup(mangled_lc_full, 0); + int ret = zend_add_literal_string(&orig); + + /* slot[1]: lowercased mangled full name (primary lookup key). */ + zend_string *lc_full = zend_string_copy(mangled_lc_full); + zend_add_literal_string(&lc_full); + + /* slot[2]: lowercased mangled unqualified name (global fallback key). */ + const char *uq; + size_t uq_len; + if (zend_get_unqualified_name(full_name, &uq, &uq_len)) { + zend_string *base_uq = zend_string_init(uq, uq_len, 0); + zend_string *mangled_lc_uq = + zend_build_concrete_turbofish_monomorph_name(base_uq, turbofish_ast); + zend_string_release(base_uq); + if (mangled_lc_uq) { + zend_add_literal_string(&mangled_lc_uq); + } else { + zend_string *dup = zend_string_copy(mangled_lc_full); + zend_add_literal_string(&dup); + } + } + + zend_string_release(mangled_lc_full); + return ret; +} + static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast *args_ast, zend_ast *turbofish_ast, uint32_t lineno, uint32_t type) /* {{{ */ { + /* Concrete turbofish ns call -> by-name dispatch to the monomorph, no VERIFY. + * Skip callable-convert (Closure creation needs the base generic function). */ + if (turbofish_ast != NULL + && args_ast->kind != ZEND_AST_CALLABLE_CONVERT) { + int mono_constants = + zend_add_ns_monomorph_name_literal(Z_STR(name_node->u.constant), turbofish_ast); + if (mono_constants != -1) { + zend_op *opline = get_next_op(); + opline->opcode = ZEND_INIT_NS_FCALL_BY_NAME; + opline->op2_type = IS_CONST; + opline->op2.constant = mono_constants; + opline->result.num = zend_alloc_cache_slot(); + zend_compile_call_common(result, args_ast, NULL, lineno, type, NULL, ZEND_VERIFY_ARITY_KIND_NONE, NULL); + return; + } + } + int name_constants = zend_add_ns_func_name_literal(Z_STR(name_node->u.constant)); /* Find frameless function with same name. */ @@ -7404,6 +7717,28 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) return; } + /* Concrete turbofish call to a known generic function -> by-name dispatch + * to the monomorph (synthesized on first INIT_FCALL_BY_NAME miss), no VERIFY. */ + if (turbofish_ast != NULL + && !is_callable_convert + && ZEND_USER_CODE(fbc->common.type) + && fbc->op_array.generic_parameters) { + zend_string *mono_lc = + zend_build_concrete_turbofish_monomorph_name(lcname, turbofish_ast); + if (mono_lc) { + zend_string_release_ex(lcname, 0); + zval_ptr_dtor(&name_node.u.constant); + zend_op *bn_opline = get_next_op(); + bn_opline->opcode = ZEND_INIT_FCALL_BY_NAME; + bn_opline->op2_type = IS_CONST; + /* zend_add_func_name_literal consumes the reference. */ + bn_opline->op2.constant = zend_add_func_name_literal(mono_lc); + bn_opline->result.num = zend_alloc_cache_slot(); + zend_compile_call_common(result, args_ast, NULL, ast->lineno, type, NULL, ZEND_VERIFY_ARITY_KIND_NONE, NULL); + return; + } + } + if (turbofish_ast == NULL && !is_callable_convert && zend_try_compile_special_func(result, lcname, diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 96698da988cd..cf748fa4ce69 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -182,6 +182,7 @@ typedef struct _zend_generic_type_table { HashTable *turbofish_args; /* opline->extended_value -> zend_type * (NAMED_WITH_ARGS holding the call-site type arguments); index is stable across optimizer reorderings */ bool persisted; /* set by opcache when the table lives in SHM/file-cache memory; suppresses destruction */ zend_generic_value_check_plan *value_check_plan; /* lazily built; request-local; NULL on persisted tables */ + struct _zend_type_arg_table *monomorph_type_args; /* on a monomorph op_array: invariant concrete type-args installed onto the call frame by dispatch; NULL otherwise */ } zend_generic_type_table; /* Compile-time linked stack of in-scope generic type parameters. */ @@ -198,6 +199,8 @@ ZEND_API zend_generic_parameter_list *zend_generic_parameter_list_alloc(uint32_t ZEND_API void zend_generic_parameter_list_destroy(zend_generic_parameter_list *list); ZEND_API zend_generic_type_table *zend_generic_type_table_alloc(void); ZEND_API void zend_generic_type_table_destroy(zend_generic_type_table *table); +ZEND_API uint32_t zend_count_generic_value_checks(const HashTable *parameters); +ZEND_API void zend_fill_generic_value_check_plan(zend_generic_value_check_plan *plan, const HashTable *parameters); ZEND_API void zend_generic_type_table_set_return(zend_generic_type_table *t, zend_type type); ZEND_API void zend_generic_type_table_set_extends(zend_generic_type_table *t, zend_type type); ZEND_API zend_generic_type_table *zend_generic_get_or_create_class_table(zend_class_entry *ce); @@ -270,6 +273,8 @@ static zend_always_inline const zend_type *zend_type_arg_entry_type(const zend_t * writable even when the entry itself is persisted to opcache SHM. */ typedef struct _zend_turbofish_args_entry { zend_type args_box; + struct _zend_type_arg_table *concrete_table; /* precomputed read-only table for a concrete INSTALL site; NULL when the handler must rebuild at runtime */ + bool concrete_skip_value_check; /* callee has no direct T-ref value params, so the value check is a no-op */ } zend_turbofish_args_entry; /* Resolve a generic call site's turbofish args_box, memoizing the (static per @@ -291,11 +296,35 @@ static zend_always_inline const zend_type *zend_generic_get_or_cache_args_box( return entry ? &entry->args_box : NULL; } +/* Like zend_generic_get_or_cache_args_box, but returns the whole entry. */ +static zend_always_inline zend_turbofish_args_entry *zend_generic_get_or_cache_args_entry( + const zend_op_array *op_array, uint32_t args_id, void **cache_slot) +{ + zend_turbofish_args_entry *entry; + if (cache_slot && cache_slot[2]) { + entry = (zend_turbofish_args_entry *) cache_slot[2]; + } else { + entry = zend_generic_get_turbofish_call_entry(op_array, args_id); + if (cache_slot && entry) { + cache_slot[2] = entry; + } + } + return entry; +} + /* Cache key sentinel for concrete-arg call sites (no T-refs in the args). * Cache key 0 means empty; CONCRETE means "args fully resolved at compile * time, table is invariant across calls." */ #define ZEND_TURBOFISH_CACHE_KEY_CONCRETE ((uintptr_t)1) +/* Runtime-promoted site: cache_slot[0] = invariant table, cache_slot[3] = + * memoized callee (low bit = value check is a no-op). */ +#define ZEND_TURBOFISH_CACHE_KEY_PROMOTED ((uintptr_t)2) + +/* Monomorphized site: cache_slot[0] = monomorph func, [3] = base func guard, + * [4] = shared invariant type-arg table. */ +#define ZEND_TURBOFISH_CACHE_KEY_MONOMORPH ((uintptr_t)3) + ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count); ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); ZEND_API zend_type_arg_table *zend_type_arg_table_capture_clone(const zend_type_arg_table *src); @@ -305,6 +334,8 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args(zend_execute_da ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type); ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, uint32_t fetch_type); ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box); +ZEND_API zend_function *zend_get_or_synthesize_call_monomorph(zend_execute_data *call, const zend_type *args_box, uint32_t arity, void **cache_slot, zend_type_arg_table **out_type_args); +ZEND_API zend_function *zend_try_monomorph_resolved_call(zend_execute_data *call, zend_type_arg_table *resolved, void **cache_slot, zend_type_arg_table **out_type_args); ZEND_API bool zend_verify_generic_return_type(zend_execute_data *call, zval *retval_ptr); typedef union _zend_parser_stack_elem { @@ -611,6 +642,12 @@ typedef struct _zend_oparray_context { /* | | | */ /* Function forbids dynamic calls | | | */ #define ZEND_ACC2_FORBID_DYN_CALLS (1 << 0) /* | X | | */ +/* | | | */ +/* op_array has generic CALL opcodes (scanned at teardown)| | | */ +#define ZEND_ACC2_HAS_GENERIC_CALL_OPS (1 << 1) /* | X | | */ +/* | | | */ +/* synthesized monomorph carrying concrete type-args | | | */ +#define ZEND_ACC2_MONOMORPH_TYPE_ARGS (1 << 2) /* | X | | */ #define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE) #define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index d7edf3372c31..2502878ff8ea 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4493,6 +4493,19 @@ static zend_never_inline void ZEND_FASTCALL init_func_run_time_cache(zend_op_arr } /* }}} */ +/* Synthesize a monomorph from a mangled name (`base`) on by-name dispatch + * miss; returns NULL when the name isn't a synthesizable generic shape. */ +ZEND_API zend_function * ZEND_FASTCALL zend_resolve_monomorph_by_name(zend_string *lc_name) /* {{{ */ +{ + zend_function *fbc = zend_try_synthesize_function_monomorph_by_name(lc_name); + if (fbc) { + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache_i(&fbc->op_array); + } + } + return fbc; +} /* }}} */ + ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function(zend_string *name) /* {{{ */ { zval *zv = zend_hash_find(EG(function_table), name); diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index fad7301033b1..ff9aff51b9d9 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -348,6 +348,10 @@ static zend_always_inline void zend_vm_init_call_frame(zend_execute_data *call, const zend_closure *closure = (const zend_closure *) ZEND_CLOSURE_OBJECT(func); call->type_args = closure->captured_type_args; + } else if (UNEXPECTED(ZEND_USER_CODE(func->type) + && (func->op_array.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS))) { + /* Monomorph reached via by-name dispatch: bind its concrete type-arg table. */ + call->type_args = func->op_array.generic_types->monomorph_type_args; } else { call->type_args = NULL; } @@ -499,6 +503,7 @@ ZEND_API zend_class_entry *zend_fetch_class_with_scope(zend_string *class_name, ZEND_API zend_class_entry *zend_fetch_class_by_name(zend_string *class_name, zend_string *lcname, uint32_t fetch_type); ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function(zend_string *name); +ZEND_API zend_function * ZEND_FASTCALL zend_resolve_monomorph_by_name(zend_string *lc_name); ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function_str(const char *name, size_t len); ZEND_API void ZEND_FASTCALL zend_init_func_run_time_cache(zend_op_array *op_array); diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index f38ad27f8351..b61e1c75f238 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -892,12 +892,15 @@ static bool zend_get_inheritance_binding( * * This is what makes property types like `T|null` reify correctly. Without the * recursive walk, a T living inside a union stays literal at runtime and the - * property type check rejects valid assignments with "of type T". */ -static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *args, uint32_t arity) + * property type check rejects valid assignments with "of type T". + * + * `origin` selects which type-parameter refs to substitute: CLASS_LIKE for the + * class monomorphizer, FUNCTION_LIKE for the function monomorphizer. */ +static zend_type zend_substitute_leaf_type_param_origin(zend_type t, const zend_type *args, uint32_t arity, uint8_t origin) { if (ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); - if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE || ref->index >= arity) { + if (ref->origin != origin || ref->index >= arity) { return t; } @@ -917,7 +920,7 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a const zend_type_named_with_args *src_nwa = ZEND_TYPE_NAMED_WITH_ARGS(t); bool needs_rebuild = false; for (uint32_t i = 0; i < src_nwa->count; i++) { - zend_type probe = zend_substitute_leaf_type_param(src_nwa->args[i], args, arity); + zend_type probe = zend_substitute_leaf_type_param_origin(src_nwa->args[i], args, arity, origin); if (memcmp(&probe, &src_nwa->args[i], sizeof(zend_type)) != 0) { needs_rebuild = true; break; @@ -931,7 +934,7 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a zend_type *new_args = (zend_type *) do_alloca(sizeof(zend_type) * src_nwa->count, use_heap); bool all_concrete = true; for (uint32_t i = 0; i < src_nwa->count; i++) { - new_args[i] = zend_substitute_leaf_type_param(src_nwa->args[i], args, arity); + new_args[i] = zend_substitute_leaf_type_param_origin(src_nwa->args[i], args, arity, origin); if (zend_type_contains_type_parameter(new_args[i])) { all_concrete = false; } @@ -976,14 +979,14 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a const zend_type *elem = &src_list->types[i]; if (ZEND_TYPE_HAS_TYPE_PARAMETER(*elem)) { const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*elem); - if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE && ref->index < arity) { + if (ref->origin == origin && ref->index < arity) { needs_rebuild = true; break; } - } else if (ZEND_TYPE_HAS_LIST(*elem)) { - /* Nested list (DNF: intersection inside union, etc.) — recurse to + } else if (ZEND_TYPE_HAS_LIST(*elem) || ZEND_TYPE_HAS_NAMED_WITH_ARGS(*elem)) { + /* Nested list (DNF) or named-with-args (`I`) — recurse to * see if there's a T-ref buried in there. */ - zend_type probe = zend_substitute_leaf_type_param(*elem, args, arity); + zend_type probe = zend_substitute_leaf_type_param_origin(*elem, args, arity, origin); if (memcmp(&probe, elem, sizeof(zend_type)) != 0) { needs_rebuild = true; break; @@ -1005,7 +1008,7 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a uint32_t out_count = 0; for (uint32_t i = 0; i < src_list->num_types; i++) { - zend_type substituted = zend_substitute_leaf_type_param(src_list->types[i], args, arity); + zend_type substituted = zend_substitute_leaf_type_param_origin(src_list->types[i], args, arity, origin); /* Keep complex elements (named types, intersection sublists, unresolved * T-refs) in the list; their scalar contribution is also OR'd into the @@ -1052,6 +1055,16 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a return result; } +static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *args, uint32_t arity) +{ + return zend_substitute_leaf_type_param_origin(t, args, arity, ZEND_GENERIC_ORIGIN_CLASS_LIKE); +} + +ZEND_API zend_type zend_substitute_function_type_param(zend_type t, const zend_type *args, uint32_t arity) +{ + return zend_substitute_leaf_type_param_origin(t, args, arity, ZEND_GENERIC_ORIGIN_FUNCTION_LIKE); +} + static bool zend_get_trait_use_binding( const zend_class_entry *ce, uint32_t trait_index, const zend_type **out_args, uint32_t *out_arity) @@ -7621,3 +7634,247 @@ ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( efree(args); return mono; } + +/* Function monomorphization: synthesize a concrete op_array for a generic + * function. The clone shares the base's refcounted body buffers (refcount==NULL + * so destroy_op_array never frees them) and only its arg_info differs. */ + +/* Build a concrete arg_info block by substituting the FUNCTION_LIKE T-refs in + * the base's pre-erasure generic types with the concrete args. */ +static zend_arg_info *zend_monomorph_build_arg_info( + const zend_op_array *base, const zend_type *args, uint32_t arity) +{ + uint32_t num_args = base->num_args; + bool has_return = (base->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + bool variadic = (base->fn_flags & ZEND_ACC_VARIADIC) != 0; + uint32_t total = num_args + (has_return ? 1 : 0) + (variadic ? 1 : 0); + if (total == 0 || !base->arg_info) { + return NULL; + } + + const zend_arg_info *orig_block = base->arg_info - (has_return ? 1 : 0); + zend_arg_info *new_block = zend_arena_alloc(&CG(arena), sizeof(zend_arg_info) * total); + memcpy(new_block, orig_block, sizeof(zend_arg_info) * total); + + const HashTable *pre_params = base->generic_types ? base->generic_types->parameters : NULL; + const zend_type *pre_return = base->generic_types ? base->generic_types->return_type : NULL; + + for (uint32_t slot = 0; slot < total; slot++) { + /* slot 0 is the return type when has_return; UINT32_MAX marks it. */ + uint32_t param_index = has_return ? (slot == 0 ? UINT32_MAX : slot - 1) : slot; + + const zend_type *pre = NULL; + if (param_index == UINT32_MAX) { + pre = pre_return; + } else if (pre_params) { + zval *zv = zend_hash_index_find(pre_params, param_index); + pre = zv ? (const zend_type *) Z_PTR_P(zv) : NULL; + } + + /* Only specialize a BARE FUNCTION_LIKE type-parameter leaf (`T $x`). + * Composite generic types keep the base's erased arg_info: the erased model + * represents generic instances by their plain class, so folding to a + * monomorph name would make RECV reject the plain instances the body emits. */ + bool is_bare_leaf = pre && ZEND_TYPE_IS_SET(*pre) + && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre) + && ZEND_TYPE_TYPE_PARAMETER(*pre)->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE; + if (is_bare_leaf) { + zend_type sub = zend_substitute_function_type_param(*pre, args, arity); + zend_type_copy_ctor(&sub, /* use_arena */ true, /* persistent */ false); + new_block[slot].type = sub; + } else { + zend_type_copy_ctor(&new_block[slot].type, /* use_arena */ true, /* persistent */ false); + } + if (new_block[slot].name) { + zend_string_addref(new_block[slot].name); + } + if (new_block[slot].doc_comment) { + zend_string_addref(new_block[slot].doc_comment); + } + } + + return new_block + (has_return ? 1 : 0); +} + +ZEND_API zend_function *zend_synthesize_function_monomorph( + zend_function *base, const zend_type *args, uint32_t arity) +{ + if (!base || base->type != ZEND_USER_FUNCTION) { + return NULL; + } + const zend_generic_parameter_list *params = base->op_array.generic_parameters; + if (!params || params->count == 0) { + return NULL; + } + + /* Fill trailing defaults so the args array covers every parameter. */ + zend_type filled[ZEND_GENERIC_MAX_PARAMS]; + uint32_t total = params->count; + if (arity > total) { + return NULL; + } + if (arity < total) { + for (uint32_t i = 0; i < arity; i++) filled[i] = args[i]; + for (uint32_t i = arity; i < total; i++) { + const zend_generic_parameter *p = ¶ms->parameters[i]; + const zend_type *def = ZEND_TYPE_IS_SET(p->default_pre_erasure) + ? &p->default_pre_erasure + : (ZEND_TYPE_IS_SET(p->default_type) ? &p->default_type : NULL); + if (!def) { + return NULL; + } + filled[i] = *def; + } + args = filled; + arity = total; + } + + /* A remaining type parameter means this isn't a concrete instantiation. */ + for (uint32_t i = 0; i < arity; i++) { + if (zend_type_contains_type_parameter(args[i])) { + return NULL; + } + } + + zend_string *display_name = zend_generic_canonical_class_name( + base->common.function_name, args, arity); + zend_string *lc_name = zend_string_tolower(display_name); + + zend_function *existing = zend_hash_find_ptr(EG(function_table), lc_name); + if (existing) { + zend_string_release(display_name); + zend_string_release(lc_name); + return existing; + } + + zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(&base->op_array, args, arity); + + zend_op_array *mono = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(mono, &base->op_array, sizeof(zend_op_array)); + + /* Invariant concrete type-arg table, shared across all calls to this monomorph + * so the body's own T-refs resolve under by-name dispatch. Arena-allocated and + * marked persisted so the refcount==NULL monomorph teardown never frees it. */ + zend_type_arg_table *mono_targs = NULL; + { + uint32_t tcount = params->count; + mono_targs = zend_arena_alloc(&CG(arena), ZEND_TYPE_ARG_TABLE_SIZE(tcount)); + mono_targs->count = tcount; + mono_targs->generation = 0; + mono_targs->persisted = true; + for (uint32_t i = 0; i < tcount; i++) { + mono_targs->entries[i].name = NULL; + mono_targs->entries[i].type_ref = NULL; + mono_targs->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); + if (i < arity && ZEND_TYPE_IS_SET(args[i])) { + zend_type owned = args[i]; + zend_type_copy_ctor(&owned, /* use_arena */ true, /* persistent */ false); + mono_targs->entries[i].owned_type = owned; + zend_string *cname = zend_type_arg_canonical_name(args[i]); + mono_targs->entries[i].name = cname; + } + } + } + + { + zend_generic_type_table *gt = zend_arena_alloc(&CG(arena), sizeof(zend_generic_type_table)); + memset(gt, 0, sizeof(*gt)); + if (base->op_array.generic_types && base->op_array.generic_types->turbofish_args) { + gt->turbofish_args = base->op_array.generic_types->turbofish_args; + } + gt->persisted = true; + gt->monomorph_type_args = mono_targs; + mono->generic_types = gt; + } + mono->fn_flags2 |= ZEND_ACC2_MONOMORPH_TYPE_ARGS; + + /* refcount==NULL: destroy_op_array won't free the shared body buffers. */ + mono->refcount = NULL; + mono->fn_flags &= ~(ZEND_ACC_IMMUTABLE | ZEND_ACC_HEAP_RT_CACHE | ZEND_ACC_PRELOADED); + /* TRAIT_CLONE forces RECV onto the slow path that checks the substituted + * arg_info (the shared RECV opcodes carry the base's erased type mask). */ + mono->fn_flags |= ZEND_ACC_TRAIT_CLONE; + + /* Keep the base name so TypeError messages match the erased path; the mangled + * name is only the EG(function_table) key. */ + mono->function_name = zend_string_copy(base->op_array.function_name); + if (new_arg_info) { + mono->arg_info = new_arg_info; + } + + ZEND_MAP_PTR_INIT(mono->run_time_cache, NULL); + ZEND_MAP_PTR_INIT(mono->static_variables_ptr, NULL); + + /* Allocate the runtime cache now: the call swaps to this op_array before + * DO_FCALL, whose hot path reads RUN_TIME_CACHE without lazy allocation. */ + zend_init_func_run_time_cache(mono); + + zend_function *mono_fn = (zend_function *) mono; + if (!zend_hash_add_ptr(EG(function_table), lc_name, mono_fn)) { + existing = zend_hash_find_ptr(EG(function_table), lc_name); + zend_string_release(display_name); + zend_string_release(lc_name); + zend_string_release(mono->function_name); + return existing; + } + + zend_string_release(display_name); + zend_string_release(lc_name); + return mono_fn; +} + +ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name) +{ + size_t lt_pos, args_len; + if (!zend_mp_split_name(lc_name, <_pos, &args_len)) return NULL; + + zend_string *base_lc = zend_string_init(ZSTR_VAL(lc_name), lt_pos, 0); + zend_function *base = zend_hash_find_ptr(EG(function_table), base_lc); + zend_string_release(base_lc); + if (!base || base->type != ZEND_USER_FUNCTION || !base->op_array.generic_parameters) { + return NULL; + } + + zend_monomorph_parser parser = { + .p = ZSTR_VAL(lc_name) + lt_pos + 1, + .end = ZSTR_VAL(lc_name) + ZSTR_LEN(lc_name) - 1, + .error = false, + }; + uint32_t cap = 4, count = 0; + zend_type *args = emalloc(sizeof(zend_type) * cap); + do { + if (count == cap) { cap *= 2; args = erealloc(args, sizeof(zend_type) * cap); } + args[count++] = zend_mp_parse_type(&parser); + if (parser.error) break; + } while (zend_mp_eat(&parser, ',')); + zend_mp_skip_ws(&parser); + if (parser.error || parser.p != parser.end) { + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return NULL; + } + + /* The by-name call skipped ZEND_VERIFY_GENERIC_ARGUMENTS, so enforce arity and + * bounds here (once); the monomorph's RECV opcodes cover per-argument checks. */ + zend_type args_box = ZEND_TYPE_INIT_NONE(0); + zend_type_named_with_args *nwa = + emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(count)); + nwa->name = NULL; + nwa->name_attr = 0; + nwa->count = count; + for (uint32_t i = 0; i < count; i++) nwa->args[i] = args[i]; + ZEND_TYPE_SET_PTR(args_box, nwa); + ZEND_TYPE_FULL_MASK(args_box) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + zend_check_generic_call_arguments(base, count, &args_box); + efree(nwa); + if (UNEXPECTED(EG(exception))) { + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return NULL; + } + + zend_function *mono = zend_synthesize_function_monomorph(base, args, count); + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return mono; +} diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index 5cc846048559..86f1460bbba0 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -109,6 +109,16 @@ static zend_always_inline bool zend_class_is_monomorph(const zend_class_entry *c ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( zend_string *name, uint32_t flags); +ZEND_API zend_type zend_substitute_function_type_param(zend_type t, const zend_type *args, uint32_t arity); + +/* Synthesize (or return the cached) concrete specialization of generic function + * `base` for the given type args, registered in EG(function_table) as + * `base`. Returns NULL when args are not concrete or base isn't generic. */ +ZEND_API zend_function *zend_synthesize_function_monomorph( + zend_function *base, const zend_type *args, uint32_t arity); + +ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name); + void zend_verify_abstract_class(zend_class_entry *ce); void zend_build_properties_info_table(zend_class_entry *ce); ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_entry *parent_ce, zend_string *lcname, zval *delayed_early_binding); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 5237caebc872..bc4d718337fe 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -189,6 +189,10 @@ static void zend_generic_type_table_value_dtor(zval *zv) { static void zend_turbofish_args_entry_dtor(zval *zv) { zend_turbofish_args_entry *entry = Z_PTR_P(zv); zend_type_release(entry->args_box, /* persistent */ false); + /* Free the heap concrete table; SHM-relocated tables have persisted set. */ + if (entry->concrete_table && !entry->concrete_table->persisted) { + zend_type_arg_table_destroy(entry->concrete_table); + } efree(entry); } @@ -315,6 +319,8 @@ ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type) { zend_turbofish_args_entry *entry = emalloc(sizeof(*entry)); entry->args_box = type; + entry->concrete_table = NULL; + entry->concrete_skip_value_check = false; zend_hash_index_update_ptr(zend_generic_type_table_ensure_turbofish(&t->turbofish_args), op_num, entry); } @@ -875,7 +881,8 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) * resolved monomorph zend_class_entry* in slot[0]. That entry is owned by * EG(class_table), NOT by the cache slot, so it must be left untouched here * — freeing it as a type_arg_table corrupts the heap. */ - if (op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { + if ((op_array->fn_flags2 & ZEND_ACC2_HAS_GENERIC_CALL_OPS) + && op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { char *cache_buf = (char *) ZEND_MAP_PTR_GET(op_array->run_time_cache); if (cache_buf) { for (uint32_t op_idx = 0; op_idx < op_array->last; op_idx++) { @@ -885,6 +892,19 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) && op->op1_type == IS_UNUSED && op->result.num) { void **cache_slot = (void **) (cache_buf + op->result.num); + /* Monomorphized site: slot[0] is a function* owned by + * EG(function_table); free only slot[4]'s type_arg table. */ + if ((uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *mt = (zend_type_arg_table *) cache_slot[4]; + if (mt) { + mt->persisted = false; + zend_type_arg_table_destroy(mt); + cache_slot[4] = NULL; + } + cache_slot[0] = NULL; + cache_slot[1] = NULL; + continue; + } zend_type_arg_table *t = (zend_type_arg_table *) cache_slot[0]; if (t) { t->persisted = false; diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index ad7d67f495d6..e2d997dd099f 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3903,7 +3903,17 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + /* Mangled monomorph name: synthesize on first reference (may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(function_name+1)); + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + } + CACHE_PTR(opline->result.num, fbc); + goto ZEND_VM_C_LABEL(fcall_by_name_push); } fbc = Z_FUNC_P(func); if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { @@ -3911,6 +3921,7 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) } CACHE_PTR(opline->result.num, fbc); } +ZEND_VM_C_LABEL(fcall_by_name_push): call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -4059,7 +4070,21 @@ ZEND_VM_HOT_HANDLER(69, ZEND_INIT_NS_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + /* Mangled monomorph name: synthesize on first reference (qualified + * then unqualified; may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 1)); + if (fbc == NULL && !EG(exception)) { + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 2)); + } + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + } + CACHE_PTR(opline->result.num, fbc); + goto ZEND_VM_C_LABEL(ns_fcall_by_name_push); } } fbc = Z_FUNC_P(func); @@ -4069,6 +4094,7 @@ ZEND_VM_HOT_HANDLER(69, ZEND_INIT_NS_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) CACHE_PTR(opline->result.num, fbc); } +ZEND_VM_C_LABEL(ns_fcall_by_name_push): call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -9079,31 +9105,121 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (OP1_TYPE == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (OP1_TYPE == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (OP1_TYPE == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -9116,6 +9232,7 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -9151,19 +9268,48 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) USE_OPLINE zend_execute_data *call = EX(call); void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + zend_turbofish_args_entry *tf_entry = + (OP1_TYPE == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (OP1_TYPE == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (OP1_TYPE == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; } - call->type_args = t; + zend_verify_generic_arg_types(call, args_box); } - zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); /* Statically pre-validated: skip the runtime arity/bound check, but @@ -9171,6 +9317,7 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 91304be18a3a..cb9d408e76c1 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4161,7 +4161,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Mangled monomorph name: synthesize on first reference (may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(function_name+1)); + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto fcall_by_name_push; } fbc = Z_FUNC_P(func); if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { @@ -4169,6 +4179,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } CACHE_PTR(opline->result.num, fbc); } +fcall_by_name_push: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -4246,7 +4257,21 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Mangled monomorph name: synthesize on first reference (qualified + * then unqualified; may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 1)); + if (fbc == NULL && !EG(exception)) { + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 2)); + } + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto ns_fcall_by_name_push; } } fbc = Z_FUNC_P(func); @@ -4256,6 +4281,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I CACHE_PTR(opline->result.num, fbc); } +ns_fcall_by_name_push: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -22261,31 +22287,121 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (IS_TMP_VAR == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_TMP_VAR == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -22298,6 +22414,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -22333,19 +22450,48 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER USE_OPLINE zend_execute_data *call = EX(call); void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + zend_turbofish_args_entry *tf_entry = + (IS_TMP_VAR == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_TMP_VAR == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + zend_verify_generic_arg_types(call, args_box); + } } else { zval *new_obj = EX_VAR(opline->op1.var); /* Statically pre-validated: skip the runtime arity/bound check, but @@ -22353,6 +22499,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); @@ -37872,31 +38019,121 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (IS_UNUSED == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_UNUSED == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -37909,6 +38146,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -37944,19 +38182,48 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER USE_OPLINE zend_execute_data *call = EX(call); void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + zend_turbofish_args_entry *tf_entry = + (IS_UNUSED == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_UNUSED == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; } - call->type_args = t; + zend_verify_generic_arg_types(call, args_box); } - zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); /* Statically pre-validated: skip the runtime arity/bound check, but @@ -37964,6 +38231,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); @@ -57609,7 +57877,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Mangled monomorph name: synthesize on first reference (may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(function_name+1)); + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto fcall_by_name_push; } fbc = Z_FUNC_P(func); if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { @@ -57617,6 +57895,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } CACHE_PTR(opline->result.num, fbc); } +fcall_by_name_push: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -57694,7 +57973,21 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_N if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Mangled monomorph name: synthesize on first reference (qualified + * then unqualified; may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 1)); + if (fbc == NULL && !EG(exception)) { + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 2)); + } + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto ns_fcall_by_name_push; } } fbc = Z_FUNC_P(func); @@ -57704,6 +57997,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_N CACHE_PTR(opline->result.num, fbc); } +ns_fcall_by_name_push: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -75507,31 +75801,121 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (IS_TMP_VAR == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_TMP_VAR == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -75544,6 +75928,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -75579,19 +75964,48 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR USE_OPLINE zend_execute_data *call = EX(call); void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + zend_turbofish_args_entry *tf_entry = + (IS_TMP_VAR == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_TMP_VAR == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + zend_verify_generic_arg_types(call, args_box); + } } else { zval *new_obj = EX_VAR(opline->op1.var); /* Statically pre-validated: skip the runtime arity/bound check, but @@ -75599,6 +76013,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); @@ -91118,31 +91533,121 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (IS_UNUSED == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_UNUSED == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); @@ -91155,6 +91660,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -91190,19 +91696,48 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR USE_OPLINE zend_execute_data *call = EX(call); void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; - const zend_type *args_box = zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); + zend_turbofish_args_entry *tf_entry = + (IS_UNUSED == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_UNUSED == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; } - call->type_args = t; + zend_verify_generic_arg_types(call, args_box); } - zend_verify_generic_arg_types(call, args_box); } else { zval *new_obj = EX_VAR(opline->op1.var); /* Statically pre-validated: skip the runtime arity/bound check, but @@ -91210,6 +91745,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index 4827a4dcd0a3..a6cd2379c2a1 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -545,6 +545,8 @@ static void zend_file_cache_serialize_turbofish_args_entry( zend_turbofish_args_entry *entry = Z_PTR_P(zv); UNSERIALIZE_PTR(entry); zend_file_cache_serialize_type(&entry->args_box, script, info, buf); + /* concrete_table is a non-portable SHM address; the load path rebuilds it. */ + entry->concrete_table = NULL; } static void zend_file_cache_serialize_generic_type_table_ht( @@ -1627,6 +1629,8 @@ static void zend_file_cache_unserialize_turbofish_args_entry( zend_turbofish_args_entry *entry = Z_PTR_P(zv); zend_file_cache_unserialize_type( &entry->args_box, zend_file_cache_generic_unserialize_scope, script, buf); + entry->concrete_table = NULL; + entry->concrete_skip_value_check = false; } static void zend_file_cache_unserialize_generic_type_table_ht( @@ -1705,6 +1709,9 @@ static void zend_file_cache_unserialize_generic_type_table( if (table->turbofish_args) { zend_file_cache_unserialize_turbofish_args_ht(&table->turbofish_args, scope, script, buf); } + + /* value_check_plan is not file-serialized; the verify path rebuilds it. */ + table->value_check_plan = NULL; } /* Mirror of zend_file_cache_serialize_type_arg_table. Rebuilds `type_ref` diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 754509c2d108..5b68782c6ebb 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -488,6 +488,47 @@ static HashTable *zend_persist_generic_type_table_ht(HashTable *ht) return ptr; } +/* Relocate a turbofish entry's concrete_table into SHM, rebinding each type_ref + * against the just-persisted copy->args_box NWA. Bails to the runtime rebuild + * path (copy->concrete_table = NULL) for any populated default slot. */ +static void zend_persist_concrete_call_table(zend_turbofish_args_entry *copy) +{ + zend_type_arg_table *src = copy->concrete_table; + if (!src) { + return; + } + + const zend_type_named_with_args *new_nwa = + ZEND_TYPE_HAS_NAMED_WITH_ARGS(copy->args_box) + ? ZEND_TYPE_NAMED_WITH_ARGS(copy->args_box) : NULL; + uint32_t passed = new_nwa ? new_nwa->count : 0; + + for (uint32_t i = 0; i < src->count; i++) { + if (i >= passed && (src->entries[i].name || src->entries[i].type_ref)) { + zend_type_arg_table_destroy(src); + copy->concrete_table = NULL; + return; + } + } + + for (uint32_t i = 0; i < src->count; i++) { + if (src->entries[i].name) { + zend_accel_store_interned_string(src->entries[i].name); + } + if (ZEND_TYPE_IS_SET(src->entries[i].owned_type)) { + zend_persist_type(&src->entries[i].owned_type); + } + src->entries[i].type_ref = + (new_nwa && src->entries[i].name && i < new_nwa->count) + ? &new_nwa->args[i] : NULL; + } + + zend_type_arg_table *persisted = zend_shared_memdup_put_free( + src, ZEND_TYPE_ARG_TABLE_SIZE(src->count)); + persisted->persisted = true; + copy->concrete_table = persisted; +} + /* Persist the turbofish_args HT. Each entry is a zend_turbofish_args_entry * which now stores only args_box — the per-call-site runtime cache that * pairs with it lives in the caller op_array's runtime cache slot, not @@ -501,6 +542,7 @@ static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) zend_turbofish_args_entry *entry = Z_PTR_P(v); zend_turbofish_args_entry *copy = zend_shared_memdup_put_free(entry, sizeof(*entry)); zend_persist_type(©->args_box); + zend_persist_concrete_call_table(copy); Z_PTR_P(v) = copy; } ZEND_HASH_FOREACH_END(); } else { @@ -512,6 +554,7 @@ static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) zend_turbofish_args_entry *entry = Z_PTR(p->val); zend_turbofish_args_entry *copy = zend_shared_memdup_put_free(entry, sizeof(*entry)); zend_persist_type(©->args_box); + zend_persist_concrete_call_table(copy); Z_PTR(p->val) = copy; } ZEND_HASH_FOREACH_END(); } @@ -617,6 +660,19 @@ static zend_generic_type_table *zend_persist_generic_type_table(zend_generic_typ persisted->turbofish_args = zend_persist_turbofish_args_ht(persisted->turbofish_args); } + /* Precompute the value-check plan into SHM (relocated via the persist arena, + * which also works in file_cache mode where no SHM segment is locked). */ + if (persisted->parameters) { + uint32_t cnt = zend_count_generic_value_checks(persisted->parameters); + size_t sz = offsetof(zend_generic_value_check_plan, checks) + + cnt * sizeof(zend_generic_value_check); + zend_generic_value_check_plan *tmp = emalloc(sz); + zend_fill_generic_value_check_plan(tmp, persisted->parameters); + persisted->value_check_plan = zend_shared_memdup_free(tmp, sz); + } else { + persisted->value_check_plan = NULL; + } + return persisted; } diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index 7a099207ea8b..be1deda11ca7 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -290,6 +290,34 @@ static void zend_persist_generic_type_table_ht_calc(HashTable *ht) ADD_SIZE(sizeof(HashTable)); } +/* Mirror zend_persist_concrete_call_table's SHM reservation; the eligibility + * predicate must stay byte-identical to persist or SHM is corrupted. */ +static void zend_persist_concrete_call_table_calc(zend_turbofish_args_entry *entry) +{ + zend_type_arg_table *src = entry->concrete_table; + if (!src) { + return; + } + const zend_type_named_with_args *nwa = + ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box) + ? ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box) : NULL; + uint32_t passed = nwa ? nwa->count : 0; + for (uint32_t i = 0; i < src->count; i++) { + if (i >= passed && (src->entries[i].name || src->entries[i].type_ref)) { + return; /* NULL-fallback: persist reserves nothing for this site. */ + } + } + ADD_SIZE(ZEND_TYPE_ARG_TABLE_SIZE(src->count)); + for (uint32_t i = 0; i < src->count; i++) { + if (src->entries[i].name) { + ADD_INTERNED_STRING(src->entries[i].name); + } + if (ZEND_TYPE_IS_SET(src->entries[i].owned_type)) { + zend_persist_type_calc(&src->entries[i].owned_type); + } + } +} + /* The turbofish_args HT entries are zend_turbofish_args_entry — same as a * boxed type for size accounting purposes, but the entry struct itself is * what we shared-memdup, not a bare zend_type. */ @@ -300,7 +328,9 @@ static void zend_persist_turbofish_args_ht_calc(HashTable *ht) zval *v; ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { ADD_SIZE(sizeof(zend_turbofish_args_entry)); - zend_persist_type_calc(&((zend_turbofish_args_entry *) Z_PTR_P(v))->args_box); + zend_turbofish_args_entry *e = (zend_turbofish_args_entry *) Z_PTR_P(v); + zend_persist_type_calc(&e->args_box); + zend_persist_concrete_call_table_calc(e); } ZEND_HASH_FOREACH_END(); } else { Bucket *p; @@ -309,7 +339,9 @@ static void zend_persist_turbofish_args_ht_calc(HashTable *ht) ADD_INTERNED_STRING(p->key); } ADD_SIZE(sizeof(zend_turbofish_args_entry)); - zend_persist_type_calc(&((zend_turbofish_args_entry *) Z_PTR(p->val))->args_box); + zend_turbofish_args_entry *e = (zend_turbofish_args_entry *) Z_PTR(p->val); + zend_persist_type_calc(&e->args_box); + zend_persist_concrete_call_table_calc(e); } ZEND_HASH_FOREACH_END(); } ADD_SIZE(sizeof(HashTable)); @@ -355,6 +387,13 @@ static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) if (table->turbofish_args) { zend_persist_turbofish_args_ht_calc(table->turbofish_args); } + + /* Mirror the value-check plan reservation in zend_persist_generic_type_table. */ + if (table->parameters) { + uint32_t cnt = zend_count_generic_value_checks(table->parameters); + ADD_SIZE(offsetof(zend_generic_value_check_plan, checks) + + cnt * sizeof(zend_generic_value_check)); + } } static void zend_persist_type_arg_table_calc(zend_type_arg_table *table) From b691feed1fd871c38bb20f3d8ab46ae4ab4c810b Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 16 Jun 2026 16:40:27 +0700 Subject: [PATCH 17/30] fix tests --- ext/reflection/php_reflection.c | 3 +++ .../tests/generics/named_type_get_name_erased.phpt | 6 +++--- ext/reflection/tests/generics/nested_args.phpt | 2 +- ext/reflection/tests/generics/property_type_args.phpt | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 680ea95bc266..0e4feebe1045 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -9060,6 +9060,9 @@ static void reflection_collect_interface_bindings( if (!intermediate || intermediate == ancestor) { continue; } + if (zend_class_is_monomorph(intermediate)) { + continue; + } if (generic_implements && zend_hash_index_exists(generic_implements, i - parent_count)) { continue; diff --git a/ext/reflection/tests/generics/named_type_get_name_erased.phpt b/ext/reflection/tests/generics/named_type_get_name_erased.phpt index cd89077b1547..91a12296d97c 100644 --- a/ext/reflection/tests/generics/named_type_get_name_erased.phpt +++ b/ext/reflection/tests/generics/named_type_get_name_erased.phpt @@ -1,5 +1,5 @@ --TEST-- -Reflection: ReflectionNamedType::getName() returns the erased name +Reflection: ReflectionNamedType::getName() returns the reified name with type arguments --FILE-- {} @@ -9,5 +9,5 @@ echo $r->getParameters()[0]->getType()->getName(), "\n"; echo $r->getReturnType()->getName(), "\n"; ?> --EXPECT-- -Box -Box +Box +Box diff --git a/ext/reflection/tests/generics/nested_args.phpt b/ext/reflection/tests/generics/nested_args.phpt index ad7c352a46bd..ba10c52ccfb1 100644 --- a/ext/reflection/tests/generics/nested_args.phpt +++ b/ext/reflection/tests/generics/nested_args.phpt @@ -12,6 +12,6 @@ $inmost = $inner->getGenericArguments()[0]; echo $inmost->getName(), "\n"; ?> --EXPECT-- -Box +Box> Box int diff --git a/ext/reflection/tests/generics/property_type_args.phpt b/ext/reflection/tests/generics/property_type_args.phpt index 786200f43bfa..d201813592c9 100644 --- a/ext/reflection/tests/generics/property_type_args.phpt +++ b/ext/reflection/tests/generics/property_type_args.phpt @@ -12,6 +12,6 @@ echo count($rt->getGenericArguments()), "\n"; echo $rt->getGenericArguments()[0]->getName(), "\n"; ?> --EXPECT-- -Box +Box 1 int From 1f95b7316dc14b64c6ac4d2781a3554c43c510b3 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 17 Jun 2026 19:37:47 +0200 Subject: [PATCH 18/30] add more benchmarks Signed-off-by: Robert Landers --- bench/bench_reification.php | 216 ++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/bench/bench_reification.php b/bench/bench_reification.php index 7bbb3ac5ff03..937730796f22 100644 --- a/bench/bench_reification.php +++ b/bench/bench_reification.php @@ -811,6 +811,222 @@ function bnd_d5(int $n): void { compare("delta: d3 vs d1 (3x deeper)", $bd3_ns, "d3", $bd1_ns, "d1"); compare("delta: d5 vs d1 (5x deeper)", $bd5_ns, "d5", $bd1_ns, "d1"); +// -------------------------------------------------------------------------- +// Scenario 24: inference cost isolation — inferred call vs turbofish vs plain. +// The commenter's concern: "inference is expensive". Inference is ~10 lines: +// peek at Z_OBJCE_P(arg)->name for each unbound inferable slot. No +// unification, no constraint-solving, no new compiler phase. +// -------------------------------------------------------------------------- + +// 24a: single-param inference with different class hierarchy depths +class Shallow {} +class Mid extends Shallow {} +class Deep extends Mid {} + +function infer_id(T $x): T { return $x; } +function plain_id(object $x): object { return $x; } + +function bench_infer_shallow(int $n): void { + $v = new Shallow(); + for ($i = 0; $i < $n; $i++) { infer_id($v); } +} +function bench_infer_deep_hierarchy(int $n): void { + $v = new Deep(); + for ($i = 0; $i < $n; $i++) { infer_id($v); } +} +function bench_turbofish_shallow(int $n): void { + $v = new Shallow(); + for ($i = 0; $i < $n; $i++) { infer_id::($v); } +} +function bench_plain_object(int $n): void { + $v = new Shallow(); + for ($i = 0; $i < $n; $i++) { plain_id($v); } +} + +// 24b: multi-param inference — the "int and float, T could be int|float" worry. +// In this implementation, each parameter maps to exactly one T slot. +// Scalars are NOT inferred (only objects). No ambiguity exists. +class TypeA {} +class TypeB {} +class TypeC {} +class TypeD {} + +function infer_two_params(T $a, U $b): T { return $a; } +function infer_three_params(T $a, U $b, V $c): T { return $a; } +function infer_four_params(T $a, U $b, V $c, W $d): T { return $a; } +function plain_four_params(TypeA $a, TypeB $b, TypeC $c, TypeD $d): TypeA { return $a; } + +function bench_infer_2param(int $n): void { + $a = new TypeA(); $b = new TypeB(); + for ($i = 0; $i < $n; $i++) { infer_two_params($a, $b); } +} +function bench_infer_3param(int $n): void { + $a = new TypeA(); $b = new TypeB(); $c = new TypeC(); + for ($i = 0; $i < $n; $i++) { infer_three_params($a, $b, $c); } +} +function bench_infer_4param(int $n): void { + $a = new TypeA(); $b = new TypeB(); $c = new TypeC(); $d = new TypeD(); + for ($i = 0; $i < $n; $i++) { infer_four_params($a, $b, $c, $d); } +} +function bench_turbofish_4param(int $n): void { + $a = new TypeA(); $b = new TypeB(); $c = new TypeC(); $d = new TypeD(); + for ($i = 0; $i < $n; $i++) { infer_four_params::($a, $b, $c, $d); } +} +function bench_plain_4param(int $n): void { + $a = new TypeA(); $b = new TypeB(); $c = new TypeC(); $d = new TypeD(); + for ($i = 0; $i < $n; $i++) { plain_four_params($a, $b, $c, $d); } +} + +// -------------------------------------------------------------------------- +// Scenario 25: inference through inheritance chains — the "expensive when +// nested in inheritance chains" worry. The monomorph for GenChild is +// synthesized once and cached in the class table. Inference reads the +// already-resolved ce->name from the object — it doesn't walk chains. +// -------------------------------------------------------------------------- +class GenBase { + public function id(T $x): T { return $x; } +} +class GenMid extends GenBase {} +class GenChild extends GenMid {} +class GenGrandchild extends GenChild {} + +function infer_from_chain(T $x): T { return $x; } + +function bench_infer_chain_d1(int $n): void { + $v = new GenBase::(); + for ($i = 0; $i < $n; $i++) { infer_from_chain($v); } +} +function bench_infer_chain_d2(int $n): void { + $v = new GenMid::(); + for ($i = 0; $i < $n; $i++) { infer_from_chain($v); } +} +function bench_infer_chain_d3(int $n): void { + $v = new GenChild::(); + for ($i = 0; $i < $n; $i++) { infer_from_chain($v); } +} +function bench_infer_chain_d4(int $n): void { + $v = new GenGrandchild::(); + for ($i = 0; $i < $n; $i++) { infer_from_chain($v); } +} + +// -------------------------------------------------------------------------- +// Scenario 26: inferred call inside a generic function (forwarding). This is +// the nested case: `outer` calls `inner(u)` which infers T=U from the +// runtime value. The inner call can't cache (inference depends on runtime +// value), but the work per call is still just a ce->name read. +// -------------------------------------------------------------------------- +function inner_infer(T $x): T { return $x; } +function outer_calls_inferred(U $x): U { return inner_infer($x); } +function outer_calls_turbofish(U $x): U { return inner_infer::($x); } +function outer_plain(Foo $x): Foo { return $x; } + +function bench_nested_infer(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { outer_calls_inferred::($f); } +} +function bench_nested_turbofish(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { outer_calls_turbofish::($f); } +} +function bench_nested_plain(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { outer_plain($f); } +} + +// -------------------------------------------------------------------------- +// Scenario 27: bounded inference — the callee has `T : SomeBase`, so after +// inferring T from the value's class, the bound must also be checked. This +// is the same covariance check as turbofish, not an extra cost of inference. +// -------------------------------------------------------------------------- +class Animal {} +class Dog extends Animal {} +class Puppy extends Dog {} + +function infer_bounded_obj(T $x): T { return $x; } +function turbofish_bounded_obj(Animal $x): Animal { return $x; } + +function bench_infer_bounded_shallow(int $n): void { + $v = new Dog(); + for ($i = 0; $i < $n; $i++) { infer_bounded_obj($v); } +} +function bench_infer_bounded_deep(int $n): void { + $v = new Puppy(); + for ($i = 0; $i < $n; $i++) { infer_bounded_obj($v); } +} +function bench_turbofish_bounded_equiv(int $n): void { + $v = new Dog(); + for ($i = 0; $i < $n; $i++) { infer_bounded_obj::($v); } +} +function bench_plain_bounded_equiv(int $n): void { + $v = new Dog(); + for ($i = 0; $i < $n; $i++) { turbofish_bounded_obj($v); } +} + +// -------------------------------------------------------------------------- +// Scenario 28: inference with monomorphised receiver in inheritance chain. +// The object's ce->name is already the resolved monomorph name (e.g. +// "GenChild"). Inference just copies that string — no chain walking. +// -------------------------------------------------------------------------- +function bench_infer_mono_receiver_d1(int $n): void { + $r = new GenBase::(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->id($acc); } +} +function bench_infer_mono_receiver_d4(int $n): void { + $r = new GenGrandchild::(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->id($acc); } +} + +section("24. Inference cost isolation"); +$is_ns = bench("infer_id(\$shallow) — inferred, flat class", 'bench_infer_shallow'); +$idh_ns = bench("infer_id(\$deep) — inferred, 3-deep class", 'bench_infer_deep_hierarchy'); +$ts_ns = bench("infer_id::(\$v) — turbofish equiv", 'bench_turbofish_shallow'); +$po_ns = bench("plain_id(\$v) — non-generic", 'bench_plain_object'); +compare("delta: inferred vs turbofish", $is_ns, "inf", $ts_ns, "tf"); +compare("delta: inferred vs plain", $is_ns, "inf", $po_ns, "plain"); +compare("delta: deep hierarchy class vs shallow", $idh_ns, "deep", $is_ns, "shallow"); + +section("24b. Multi-param inference scaling"); +$i2_ns = bench("infer_two_params(\$a,\$b) — 2 params inferred", 'bench_infer_2param'); +$i3_ns = bench("infer_three_params(\$a,\$b,\$c) — 3 params inferred", 'bench_infer_3param'); +$i4_ns = bench("infer_four_params(\$a...\$d) — 4 params inferred", 'bench_infer_4param'); +$t4_ns = bench("turbofish::(\$a...) — 4 params turbofish", 'bench_turbofish_4param'); +$p4_ns = bench("plain_four_params(\$a...\$d) — non-generic", 'bench_plain_4param'); +compare("delta: 4-param infer vs 2-param infer", $i4_ns, "4p", $i2_ns, "2p"); +compare("delta: 4-param infer vs 4-param turbo", $i4_ns, "inf", $t4_ns, "tf"); +compare("delta: 4-param infer vs 4-param plain", $i4_ns, "inf", $p4_ns, "plain"); + +section("25. Inference through inheritance chains"); +$ic1_ns = bench("infer_from_chain(GenBase) — chain depth 1", 'bench_infer_chain_d1'); +$ic2_ns = bench("infer_from_chain(GenMid) — chain depth 2", 'bench_infer_chain_d2'); +$ic3_ns = bench("infer_from_chain(GenChild) — chain depth 3", 'bench_infer_chain_d3'); +$ic4_ns = bench("infer_from_chain(GenGrandchild) — chain depth 4", 'bench_infer_chain_d4'); +compare("delta: chain d2 vs chain d1", $ic2_ns, "d2", $ic1_ns, "d1"); +compare("delta: chain d4 vs chain d1", $ic4_ns, "d4", $ic1_ns, "d1"); + +section("26. Nested generic call with inner inference"); +$ni_ns = bench("outer → inner inferred — inner infers", 'bench_nested_infer'); +$nt_ns = bench("outer → inner:: turbofish — inner forwarded",'bench_nested_turbofish'); +$np_ns = bench("outer_plain(\$f) — non-generic", 'bench_nested_plain'); +compare("delta: nested infer vs nested turbo", $ni_ns, "inf", $nt_ns, "tf"); +compare("delta: nested infer vs plain", $ni_ns, "inf", $np_ns, "plain"); +compare("delta: nested turbo vs plain", $nt_ns, "tf", $np_ns, "plain"); + +section("27. Bounded inference (covariance check)"); +$ibs_ns = bench("infer_bounded(Dog) — shallow subtype", 'bench_infer_bounded_shallow'); +$ibd_ns = bench("infer_bounded(Puppy) — deeper subtype", 'bench_infer_bounded_deep'); +$tbe_ns = bench("infer_bounded::(Dog) — turbofish+bound", 'bench_turbofish_bounded_equiv'); +$pbe_ns = bench("plain_bounded(Dog) — non-generic typed", 'bench_plain_bounded_equiv'); +compare("delta: infer+bound vs turbofish+bound", $ibs_ns, "inf", $tbe_ns, "tf"); +compare("delta: infer+bound vs plain typed", $ibs_ns, "inf", $pbe_ns, "plain"); +compare("delta: deeper subtype vs shallow subtype", $ibd_ns, "deep", $ibs_ns, "shallow"); + +section("28. Method dispatch on monomorphised inheritance chain receiver"); +$mrd1_ns = bench("GenBase->id() — chain depth 1", 'bench_infer_mono_receiver_d1'); +$mrd4_ns = bench("GenGrandchild->id() — chain depth 4", 'bench_infer_mono_receiver_d4'); +compare("delta: chain d4 receiver vs chain d1", $mrd4_ns, "d4", $mrd1_ns, "d1"); + $opcache_loaded = extension_loaded('Zend OPcache'); $opcache_on = $opcache_loaded && function_exists('opcache_get_status') From 21fd39ee90ee3c7a14ccc4956ee75a6df6624a31 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 17 Jun 2026 20:47:47 +0200 Subject: [PATCH 19/30] opcache: don't JIT reified-generic monomorphs (shared-opcode aliasing) Turbofish monomorphs are shallow copies of their base op_array that share the opcode buffer (zend_synthesize_function_monomorph). The tracing JIT keys compiled code and patched opline handlers on opcode addresses and bakes op_array-specific constants (arg_info for RECV / VERIFY_RETURN_TYPE, inferred operand types, cached literals), which are valid for only one substituted arg_info layout. A trace compiled for one monomorph was therefore reused for a sibling carrying a different binding, enforcing the wrong type: e.g. nestedGen::($fb) was checked against an earlier nestedGen:: binding and wrongly threw "must be of type string|int" under opcache.jit=tracing (the interpreter and jit=disable were correct). The interpreter avoids this via the TRAIT_CLONE slow-path RECV; the JIT has no equivalent and the hazard is not limited to RECV, so keep monomorphizable generics and their monomorphs interpreted until per-monomorph JIT state exists. Signed-off-by: Robert Landers Co-Authored-By: Claude Opus 4.8 --- .../jit_monomorph_no_crosstalk.phpt | 51 +++++++++++++++++++ ext/opcache/jit/zend_jit.c | 19 +++++++ 2 files changed, 70 insertions(+) create mode 100644 Zend/tests/generics/reification/jit_monomorph_no_crosstalk.phpt diff --git a/Zend/tests/generics/reification/jit_monomorph_no_crosstalk.phpt b/Zend/tests/generics/reification/jit_monomorph_no_crosstalk.phpt new file mode 100644 index 000000000000..89de420c1222 --- /dev/null +++ b/Zend/tests/generics/reification/jit_monomorph_no_crosstalk.phpt @@ -0,0 +1,51 @@ +--TEST-- +JIT: turbofish monomorphs sharing a base op_array must not alias each other's type checks +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +--FILE-- +(T $x): T { return $x; } +function nestedGen(U $x): U { return idGen::($x); } + +function call_scalar(int $n): void { for ($i = 0; $i < $n; $i++) { nestedGen::($i); } } +function call_class(int $n): void { $f = new Foo(); for ($i = 0; $i < $n; $i++) { nestedGen::($f); } } +function call_union(int $n): void { for ($i = 0; $i < $n; $i++) { nestedGen::($i); } } +function call_inter(int $n): void { $fb = new FooBar(); for ($i = 0; $i < $n; $i++) { nestedGen::($fb); } } + +/* Each concrete turbofish call synthesizes a monomorph of nestedGen with its + * own substituted arg_info, but all monomorphs share the base's opcode buffer. + * The tracing JIT keys compiled code on opline addresses and bakes arg_info as + * a constant; before the fix it compiled one binding's trace and reused it for + * the others, so the intersection call below was checked against the earlier + * union binding (int|string) and wrongly threw. Run the bindings in the order + * that made nestedGen hot under the scalar/class bindings first. */ +bench('call_scalar', 100000); +bench('call_class', 100000); +bench('call_union', 20000); +bench('call_inter', 20000); + +$fb = new FooBar(); +var_dump(nestedGen::($fb) === $fb); +var_dump(nestedGen::(7)); +echo "done\n"; +?> +--EXPECT-- +bool(true) +int(7) +done diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index 8db1b17943c2..4ddad984ff7d 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3315,6 +3315,25 @@ int zend_jit_op_array(zend_op_array *op_array, zend_script *script) return FAILURE; } + /* Reified-generic monomorphs are shallow copies of their base op_array that + * SHARE the opcode buffer (see zend_synthesize_function_monomorph). The JIT + * keys compiled code and patched opline handlers on opcode addresses and + * bakes op_array-specific constants (arg_info for RECV / VERIFY_RETURN_TYPE, + * inferred operand types, cached literals). Those are valid for exactly one + * arg_info layout, but every monomorph that shares these opcodes carries a + * DIFFERENT substituted layout — so compiling the base (or any one + * monomorph) corrupts its siblings: a sibling's call enters a trace baked + * for a different binding and the wrong type is enforced. The interpreter + * avoids this through the TRAIT_CLONE slow-path RECV; the JIT has no + * equivalent. Until per-monomorph JIT state exists, keep monomorphizable + * generics and their monomorphs interpreted. */ + if (op_array->generic_parameters + || (op_array->fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + || (op_array->scope && op_array->scope->generic_parameters)) { + ZEND_SET_FUNC_INFO(op_array, NULL); + return SUCCESS; + } + if (JIT_G(trigger) == ZEND_JIT_ON_FIRST_EXEC) { zend_jit_op_array_extension *jit_extension; zend_op *opline = op_array->opcodes; From bfbbe37682b30894d35f0277b754d0af9a1f3e25 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 17 Jun 2026 20:56:43 +0200 Subject: [PATCH 20/30] Zend: erase nested generic type argument when substituting a bare T leaf When a bare type-parameter leaf (`T $x`) is bound to a concrete generic instantiation via turbofish (e.g. `id::>>($v)`), the binding arrives as a pre-erasure named-with-args type. zend_substitute_leaf_type_param_origin returned it verbatim, so the synthesized monomorph's arg_info (and return type) kept the named-with-args payload. The reified RECV check then reached zend_fetch_ce_from_type(), which reads ZEND_TYPE_NAME() and interpreted that payload as a zend_string: at depth >= 2 it dereferenced a garbage length and tried a multi-terabyte allocation; at depth 1 it raised a spurious TypeError. Fold a fully concrete named-with-args binding to a plain CLASS reference to the monomorph's canonical class name, mirroring the named-with-args branch in the same function. That is the erased shape the runtime arg / property / return checks already understand, and it matches the class the value itself carries (so inference and turbofish now agree). Reproduces in the interpreter and under opcache; not JIT-specific. Signed-off-by: Robert Landers Co-Authored-By: Claude Opus 4.8 --- .../turbofish/nested_type_arg_erasure.phpt | 32 +++++++++++++++++++ Zend/zend_inheritance.c | 21 ++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 Zend/tests/generics/turbofish/nested_type_arg_erasure.phpt diff --git a/Zend/tests/generics/turbofish/nested_type_arg_erasure.phpt b/Zend/tests/generics/turbofish/nested_type_arg_erasure.phpt new file mode 100644 index 000000000000..e338c3a408ad --- /dev/null +++ b/Zend/tests/generics/turbofish/nested_type_arg_erasure.phpt @@ -0,0 +1,32 @@ +--TEST-- +Turbofish with a nested generic type argument erases the substituted leaf to the monomorph class +--FILE-- + {} +class L3 {} +class DBox {} + +function id_gen(T $x): T { return $x; } + +$d1 = new DBox::(); +$d2 = new DBox::>(); +$d3 = new DBox::>>(); + +/* T is a bare leaf substituted with a concrete generic instantiation. The + * binding arrives as a pre-erasure named-with-args type; it must be folded to + * the monomorph's canonical class name so the reified RECV / return checks see + * the same erased shape the value carries. Before the fix, depth >= 2 read the + * named-with-args payload as a class-name string (bogus huge allocation) and + * depth 1 raised a spurious TypeError. */ +var_dump(id_gen::>($d1) === $d1); +var_dump(id_gen::>>($d2) === $d2); +var_dump(id_gen::>>>($d3) === $d3); + +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +done diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index b61e1c75f238..c355fef2cf30 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -905,6 +905,27 @@ static zend_type zend_substitute_leaf_type_param_origin(zend_type t, const zend_ } zend_type result = args[ref->index]; + + /* When the binding is itself a concrete generic instantiation + * (e.g. `T = DBox>`, supplied as a pre-erasure + * NAMED_WITH_ARGS type), fold it to a plain CLASS reference to the + * monomorph — the erased shape the runtime arg/property/return checks + * understand. This mirrors the named-with-args branch below; without + * it the substituted leaf keeps its NWA payload and zend_fetch_ce_from_type + * reads that payload as a zend_string, dereferencing garbage (a bogus + * multi-terabyte allocation, or a spurious TypeError at shallower depth). */ + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(result) + && !zend_type_contains_type_parameter(result)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(result); + zend_string *canonical = zend_generic_canonical_class_name( + nwa->name, nwa->args, nwa->count); + bool result_nullable = (ZEND_TYPE_FULL_MASK(result) & _ZEND_TYPE_NULLABLE_BIT) != 0; + result = (zend_type) ZEND_TYPE_INIT_CLASS(canonical, 0, 0); + if (result_nullable) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + } + if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; } From 13d7ac80004a9e2d6bb8a2636a9fd23cc7c15d5a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 17 Jun 2026 21:09:52 +0200 Subject: [PATCH 21/30] Zend: by-name monomorph lookup must not raise on invalid type arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zend_try_synthesize_monomorph_by_name() backs the class-existence lookup path (class_exists(), interface_exists(), is-a probes, the autoloader). It raised in two cases: a non-generic base carrying type arguments ("Plain") threw, and an out-of-bound argument ("Box" for Box) reached zend_synthesize_monomorph(), whose bound check fatals via zend_error_noreturn. So class_exists('Box') aborted with a fatal instead of returning false. A name whose arguments are invalid simply names a class that does not exist, so the lookup now reports "not found" (NULL): the non-generic case returns NULL directly, and bounds are validated silently up front, returning NULL on violation before synthesis. The throwing enforcement stays on the `new` / type-declaration paths (compile-time and zend_synthesize_monomorph), where a bound violation is a genuine error — verified that `new Box::()` and a `Plain` type position still fatal. Signed-off-by: Robert Landers Co-Authored-By: Claude Opus 4.8 --- .../class_exists_invalid_monomorph_name.phpt | 35 +++++++++++++++++++ Zend/zend_inheritance.c | 25 +++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 Zend/tests/generics/runtime/class_exists_invalid_monomorph_name.phpt diff --git a/Zend/tests/generics/runtime/class_exists_invalid_monomorph_name.phpt b/Zend/tests/generics/runtime/class_exists_invalid_monomorph_name.phpt new file mode 100644 index 000000000000..ff42a5abfcac --- /dev/null +++ b/Zend/tests/generics/runtime/class_exists_invalid_monomorph_name.phpt @@ -0,0 +1,35 @@ +--TEST-- +class_exists()/interface_exists() return false (never raise) for out-of-bound or non-generic monomorph names +--FILE-- + {} +interface Iface {} + +// Bound violation: int is not an object, so "Box" names no class. A +// passive existence probe must report that as false, not raise (the `new` / +// type-declaration paths still enforce the bound with a fatal). +var_dump(class_exists('Box')); +// Bound satisfied -> the monomorph is synthesized on demand. +var_dump(class_exists('Box')); +// Type arguments on a non-generic class name no existing class either. +var_dump(class_exists('Plain')); +// Plain lookups are unaffected. +var_dump(class_exists('Plain')); +var_dump(class_exists('Box')); +// interface_exists() behaves the same way. +var_dump(interface_exists('Iface')); +var_dump(interface_exists('Iface')); + +echo "done\n"; +?> +--EXPECT-- +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(false) +bool(true) +done diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index c355fef2cf30..85840d649c62 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -7625,9 +7625,10 @@ ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( zend_string_release(base_name); if (!base) return NULL; if (!base->generic_parameters) { - zend_throw_error(NULL, - "Type arguments are not allowed on non-generic class %s", - ZSTR_VAL(base->name)); + /* A name like "Plain" where Plain is non-generic does not name an + * existing class. A by-name lookup reports that as "not found" (NULL) + * rather than raising; type arguments in a real type/`new` position are + * rejected at compile time on their own path. */ return NULL; } @@ -7650,6 +7651,24 @@ ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( return NULL; } + /* A lookup by name (class_exists(), is-a probes, the autoloader) must not + * raise: a monomorph name whose arguments violate the declared bounds simply + * names a class that does not exist. Validate the supplied arguments against + * the base's bounds here and report "not found" (NULL) on violation, instead + * of letting zend_synthesize_monomorph fatal as the `new` / type-declaration + * paths intentionally do. */ + uint32_t bound_check_count = count < base->generic_parameters->count + ? count : base->generic_parameters->count; + for (uint32_t i = 0; i < bound_check_count; i++) { + zend_type bound = base->generic_parameters->parameters[i].bound; + if (ZEND_TYPE_IS_SET(bound) + && zend_check_generic_arg_satisfies_bound(base, args[i], base, bound) != INHERITANCE_SUCCESS) { + for (uint32_t j = 0; j < count; j++) zend_type_release(args[j], false); + efree(args); + return NULL; + } + } + zend_class_entry *mono = zend_synthesize_monomorph(base, args, count); for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); efree(args); From 96cc3618f70728fa2b87f24b3a762e280c8ecc65 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 18 Jun 2026 16:31:46 +0200 Subject: [PATCH 22/30] fix inference passing through Signed-off-by: Robert Landers --- .../reification/inference_beats_default.phpt | 76 +++++++++++++++++++ Zend/zend_compile.c | 69 +++++++++++++---- 2 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 Zend/tests/generics/reification/inference_beats_default.phpt diff --git a/Zend/tests/generics/reification/inference_beats_default.phpt b/Zend/tests/generics/reification/inference_beats_default.phpt new file mode 100644 index 000000000000..fc2c6a872a32 --- /dev/null +++ b/Zend/tests/generics/reification/inference_beats_default.phpt @@ -0,0 +1,76 @@ +--TEST-- +Reification: value-directed inference wins over a type parameter's declared default +--FILE-- + { + public function __construct(public T $value) {} +} + +function foo(A $a): array { + return func_get_type_args(); +} + +// Inferred from the argument, not the `mixed` default. +var_dump(foo(new Foo())); +var_dump(foo(new Bar())); + +// Explicit turbofish still wins over inference: A is pinned to Bar even though +// the argument's runtime class is Sub (which satisfies the Bar binding). +var_dump(foo::(new Sub())); + +// The default is still used when nothing pins the slot: U is not a value +// parameter, so it can't be inferred and falls back to its default. +function pick(A $a): array { + return func_get_type_args(); +} +var_dump(pick(new Bar())); + +// Partial turbofish: A is given explicitly, B is inferred from its argument +// even though B has a default — the monomorph fast path must defer to the +// inference path here. +function two(A $a, B $b): array { + return func_get_type_args(); +} +var_dump(two::(new Bar(), new Foo())); + +// Return type Box reifies to the inferred A, not Box. The forwarded +// turbofish `new Box::` resolves A against the (now inferred) binding. +function wrap(A $a): Box { + return new Box::($a); +} +$b = wrap(new Foo()); +var_dump($b::class); +?> +--EXPECT-- +array(1) { + ["A"]=> + string(3) "Foo" +} +array(1) { + ["A"]=> + string(3) "Bar" +} +array(1) { + ["A"]=> + string(3) "Bar" +} +array(2) { + ["A"]=> + string(3) "Bar" + ["U"]=> + string(3) "Foo" +} +array(2) { + ["A"]=> + string(3) "Bar" + ["B"]=> + string(3) "Foo" +} +string(8) "Box" diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 680da3c5ee88..6e47f1b6ed78 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -699,7 +699,12 @@ static uintptr_t zend_compute_call_cache_key( * i.e. no inference will fire that could plug an unset slot from a runtime * value. Inference depends on argument classes — not part of the key — so a * call that triggers it must be excluded from caching even if the - * caller-binding fingerprint matches. */ + * caller-binding fingerprint matches. + * + * Any inferable slot not pinned by a turbofish arg makes the call uncacheable, + * even when the slot has a declared default: inference outranks the default + * (see the phase order in zend_build_generic_call_type_args), so an object + * argument still resolves the slot from a runtime value. */ static bool zend_call_is_cacheable_against_args( const zend_function *fbc, const zend_type *args_box) { @@ -715,7 +720,6 @@ static bool zend_call_is_cacheable_against_args( for (uint32_t i = 0; i < params->count && i < 64; i++) { if (!(inferable & ((uint64_t) 1 << i))) continue; if (i < passed) continue; - if (ZEND_TYPE_IS_SET(params->parameters[i].default_type)) continue; return false; } return true; @@ -831,6 +835,15 @@ ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { return NULL; } + + /* A partial turbofish that leaves an inferable slot unset must NOT be + * monomorphized on the turbofish args alone: the unset slot is filled by + * value-directed inference (a runtime value), so the resolved signature + * varies per call. Fall back to the erased VERIFY path, which infers. */ + if (!zend_call_is_cacheable_against_args(base, args_box)) { + return NULL; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); /* Enforce arity + bounds as the erased VERIFY path does (first call only). */ @@ -1055,9 +1068,13 @@ ZEND_API void zend_apply_generic_new( } /* Slots left NULL mean "fall back to the parameter's bound". Order of resolution - * for each slot: explicit turbofish arg → parameter's declared default → - * value-directed inference from any argument whose pre-erasure type is a - * direct TYPE_PARAMETER reference to this slot. */ + * for each slot (highest precedence first): explicit turbofish arg → + * value-directed inference from any argument whose pre-erasure type is a direct + * TYPE_PARAMETER reference to this slot → parameter's declared default. The + * default is the *lowest* precedence: it only fills a slot that neither an + * explicit turbofish arg nor inference could pin, so `function f(A $a)` + * called as `f($x)` reifies A from $x rather than collapsing to the `mixed` + * default. */ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( zend_execute_data *call, const zend_type *args_box) { @@ -1082,14 +1099,11 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( } zend_execute_data *caller = EG(current_execute_data); - for (uint32_t i = 0; i < params->count; i++) { - const zend_type *src = NULL; - if (i < passed) { - src = &passed_args[i]; - } else if (ZEND_TYPE_IS_SET(params->parameters[i].default_type)) { - src = ¶ms->parameters[i].default_type; - } - if (!src || !ZEND_TYPE_IS_SET(*src)) { + /* Phase 1: explicit turbofish args (highest precedence). Defaults are + * deferred to phase 3 so inference can pin an inferable slot first. */ + for (uint32_t i = 0; i < passed && i < params->count; i++) { + const zend_type *src = &passed_args[i]; + if (!ZEND_TYPE_IS_SET(*src)) { continue; } /* A turbofish arg can itself name an outer generic parameter (e.g. @@ -1116,7 +1130,7 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( table->entries[i].type_ref = src; } - /* Inference for unset slots. Composite shapes (array, Box, ?T) are + /* Phase 2: inference for unset slots. Composite shapes (array, Box, ?T) are * skipped — only bare top-level T at a value-parameter position. The * inferable_mask short-circuits when no inferable slot remains unset. */ uint64_t unbound_inferable = params->inferable_mask; @@ -1155,6 +1169,33 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( } ZEND_HASH_FOREACH_END(); } + /* Phase 3: declared defaults (lowest precedence) — fill only slots that + * neither a turbofish arg nor inference pinned. A default may itself name + * an outer generic parameter, resolved against the caller frame exactly as + * a turbofish arg is in phase 1. */ + for (uint32_t i = 0; i < params->count; i++) { + if (table->entries[i].name) { + continue; + } + const zend_type *src = ¶ms->parameters[i].default_type; + if (!ZEND_TYPE_IS_SET(*src)) { + continue; + } + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*src)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*src); + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE + && caller && caller->type_args + && ref->index < caller->type_args->count + && caller->type_args->entries[ref->index].name) { + table->entries[i].name = zend_string_copy(caller->type_args->entries[ref->index].name); + table->entries[i].type_ref = zend_type_arg_entry_type(&caller->type_args->entries[ref->index]); + } + continue; + } + table->entries[i].name = zend_type_arg_canonical_name(*src); + table->entries[i].type_ref = src; + } + return table; } From c21b571e35541d2f5c1c5222a6e74c07f31f65b4 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 18 Jun 2026 17:26:19 +0200 Subject: [PATCH 23/30] prevent monomorphed methods from colliding with unrelated classes that happen to have the same name Signed-off-by: Robert Landers --- ...od_monomorph_no_cross_class_collision.phpt | 36 +++++++++++++++++++ Zend/zend_inheritance.c | 17 +++++++++ 2 files changed, 53 insertions(+) create mode 100644 Zend/tests/generics/reification/method_monomorph_no_cross_class_collision.phpt diff --git a/Zend/tests/generics/reification/method_monomorph_no_cross_class_collision.phpt b/Zend/tests/generics/reification/method_monomorph_no_cross_class_collision.phpt new file mode 100644 index 000000000000..8595207d03ee --- /dev/null +++ b/Zend/tests/generics/reification/method_monomorph_no_cross_class_collision.phpt @@ -0,0 +1,36 @@ +--TEST-- +Reification: same-named generic methods in different classes get distinct monomorphs +--FILE-- + +// and F::pick collide and the second class's call dispatches to the +// first class's body. +class E { public function pick($x) { return 'E::pick'; } } +class F { public function pick($x) { return 'F::pick'; } } + +var_dump((new E)->pick(1)); +var_dump((new F)->pick(1)); + +// Order-independent: resolving F first must not poison E either. +class G { public function take($x) { return 'G::take'; } } +class H { public function take($x) { return 'H::take'; } } + +var_dump((new H)->take(1)); +var_dump((new G)->take(1)); + +// Same method called twice on the same class still reuses one monomorph. +var_dump((new E)->pick(2)); + +// Turbofish form must also stay class-distinct. +var_dump((new E)->pick::(1)); +var_dump((new F)->pick::(1)); +?> +--EXPECT-- +string(7) "E::pick" +string(7) "F::pick" +string(7) "H::take" +string(7) "G::take" +string(7) "E::pick" +string(7) "E::pick" +string(7) "F::pick" diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 85840d649c62..db64af028da0 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -7780,6 +7780,23 @@ ZEND_API zend_function *zend_synthesize_function_monomorph( base->common.function_name, args, arity); zend_string *lc_name = zend_string_tolower(display_name); + /* A method's mangled name carries only the method name + type args, so the + * same method name in two classes collides (E::pick<...> and F::pick<...> + * both mangle to pick<...>). Qualify the EG(function_table) key with the + * lowercased declaring scope so distinct classes get distinct monomorphs. + * The "::" can never appear in a free-function monomorph name, so a + * scope-qualified key is never mistaken for a by-name-dispatchable one. */ + if (base->common.scope) { + zend_string *lc_scope = zend_string_tolower(base->common.scope->name); + zend_string *scoped = zend_string_concat3( + ZSTR_VAL(lc_scope), ZSTR_LEN(lc_scope), + "::", 2, + ZSTR_VAL(lc_name), ZSTR_LEN(lc_name)); + zend_string_release(lc_scope); + zend_string_release(lc_name); + lc_name = scoped; + } + zend_function *existing = zend_hash_find_ptr(EG(function_table), lc_name); if (existing) { zend_string_release(display_name); From 60a7eac7f5bd6c311cc995ea3b97c185ebebf043 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 18 Jun 2026 17:58:55 +0200 Subject: [PATCH 24/30] fix union of T|null Signed-off-by: Robert Landers --- .../reification/union_t_null_pre_erasure.phpt | 49 +++++++++++++++++++ Zend/zend_compile.c | 15 ++++++ 2 files changed, 64 insertions(+) create mode 100644 Zend/tests/generics/reification/union_t_null_pre_erasure.phpt diff --git a/Zend/tests/generics/reification/union_t_null_pre_erasure.phpt b/Zend/tests/generics/reification/union_t_null_pre_erasure.phpt new file mode 100644 index 000000000000..507a1f2b0ab9 --- /dev/null +++ b/Zend/tests/generics/reification/union_t_null_pre_erasure.phpt @@ -0,0 +1,49 @@ +--TEST-- +Reification: `T|null` and `?T` are the same pre-erasure shape across abstract/interface inheritance +--FILE-- + { + public function get(TK|null $k): ?TV; +} +class C implements I { + public function get(TK|null $k): ?TV { return $k; } +} + +// Same shape via an abstract class. +abstract class AB { + abstract public function get(TK|null $k): ?TV; +} +class CB extends AB { + public function get(TK|null $k): ?TV { return $k; } +} + +// Mixed syntax: proto uses `?TK`, child uses `TK|null` — must still match. +interface J { + public function get(?TK $k): ?TV; +} +class D implements J { + public function get(TK|null $k): ?TV { return $k; } +} + +echo "compiled ok\n"; + +// The reified binding is still enforced at runtime for the union param form. +$c = new C::(); +var_dump($c->get(null)); // null accepted by `TK|null` == `?int` +var_dump($c->get(5)); + +try { + $c->get("not-an-int"); +} catch (TypeError $e) { + echo "TypeError: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +compiled ok +NULL +int(5) +TypeError: C::get(): Argument #1 ($k)%smust be of type ?int, string given%a diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6e47f1b6ed78..cc05ff8c9478 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2078,6 +2078,21 @@ static zend_type zend_compile_pre_erasure_typename(zend_ast *ast) /* All members folded into the scalar mask. */ efree(type_list); ZEND_TYPE_FULL_MASK(result) |= merged_scalar_mask; + } else if (is_union && out_count == 1 + && (merged_scalar_mask & _ZEND_TYPE_MAY_BE_MASK & ~_ZEND_TYPE_NULLABLE_BIT) == 0) { + /* Degenerate union `T|null` (or `Box|null`): collapse to the + * single complex member carrying the nullable bit, the same shape + * `?T` compiles to and that zend_substitute_leaf_type_param produces + * for the substituted form. Keeping a one-element LIST here makes + * `T|null` diverge from `?T` in the inheritance variance check: the + * LIST escapes the bare-type-parameter fallback `?T` takes, so a + * child's `T|null` ends up compared against the parent's erased + * `mixed` and is wrongly rejected. */ + result = type_list->types[0]; + if (merged_scalar_mask & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + efree(type_list); } else { type_list->num_types = out_count; ZEND_TYPE_SET_PTR(result, type_list); From 5d527a6987a61af0d16f2925a9fef4b4e58233b6 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 18 Jun 2026 21:33:50 +0200 Subject: [PATCH 25/30] fix new bare() and return an error when types cannot be determined -- also make monomorphs share static props Signed-off-by: Robert Landers --- .../variance/self_ref_class_name_ok.phpt | 2 +- .../diamond/forwarding_t_vs_concrete.phpt | 2 +- .../naked_new_runtime_declared_class.phpt | 56 +++ .../runtime/lsb_through_monomorph.phpt | 9 +- .../naked_new_no_defaults_self_vs_byname.phpt | 42 ++ .../naked_new_self_no_defaults_lenient.phpt | 27 -- .../runtime/static_property_isolation.phpt | 47 -- .../runtime/static_property_shared.phpt | 51 +++ .../generics/scoping/trait_use_clause.phpt | 9 +- .../traits/trait_use_args_compile.phpt | 4 +- Zend/zend_inheritance.c | 36 +- Zend/zend_inheritance.h | 7 + Zend/zend_vm_def.h | 72 ++- Zend/zend_vm_execute.h | 432 +++++++++++++----- 14 files changed, 545 insertions(+), 251 deletions(-) create mode 100644 Zend/tests/generics/reification/naked_new_runtime_declared_class.phpt create mode 100644 Zend/tests/generics/runtime/naked_new_no_defaults_self_vs_byname.phpt delete mode 100644 Zend/tests/generics/runtime/naked_new_self_no_defaults_lenient.phpt delete mode 100644 Zend/tests/generics/runtime/static_property_isolation.phpt create mode 100644 Zend/tests/generics/runtime/static_property_shared.phpt diff --git a/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt b/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt index 9c88343c7ab8..288c8794079e 100644 --- a/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt +++ b/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt @@ -5,7 +5,7 @@ Variance: a generic class referencing itself by name in a covariant return posit final readonly class Pair<+L, +R> { public function __construct(public L $left, public R $right) {} public function swap(): Pair { - return new Pair($this->right, $this->left); + return new Pair::($this->right, $this->left); } } diff --git a/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt b/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt index f0887f4b7523..f0da6a4ebf9e 100644 --- a/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt +++ b/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt @@ -6,7 +6,7 @@ interface Box {} interface Wrapper extends Box {} class C implements Wrapper, Box {} -$c = new C(); +$c = new C::(); var_dump($c instanceof Box, $c instanceof Wrapper); ?> --EXPECT-- diff --git a/Zend/tests/generics/reification/naked_new_runtime_declared_class.phpt b/Zend/tests/generics/reification/naked_new_runtime_declared_class.phpt new file mode 100644 index 000000000000..a0a64c2d4a7d --- /dev/null +++ b/Zend/tests/generics/reification/naked_new_runtime_declared_class.phpt @@ -0,0 +1,56 @@ +--TEST-- +Reification: naked `new C()` synthesizes the defaults monomorph even when C is declared at runtime (trait / parameterized implements) +--FILE-- + { + use AnyTrait; + public array $items = []; + public function __construct($i = []) { $this->items = (array) $i; } + public function copy(): static { return new static($this->items); } +} + +$a = new C([1, 2]); +echo $a::class, "\n"; // C, not bare C +echo $a->copy()::class, "\n"; // C +var_dump($a::class === $a->copy()::class); +var_dump((new C([1, 2])) == (new C([1, 2]))->copy()); + +// Parameterized implements also defers declaration to runtime. +interface Iface {} +class D implements Iface { + public function __construct(public int $n = 0) {} + public function dup(): static { return new static($this->n); } +} +$d = new D(7); +echo $d::class, "\n"; // D +var_dump($d::class === $d->dup()::class); +var_dump($d instanceof Iface); + +// Control: a class with neither trait nor parameterized implements is +// early-bound; the compile-time rewrite already produces the monomorph. +class E { + public function __construct(public array $items = []) {} + public function copy(): static { return new static($this->items); } +} +$e = new E([1]); +echo $e::class, "\n"; // E +var_dump($e::class === $e->copy()::class); +?> +--EXPECT-- +C +C +bool(true) +bool(true) +D +bool(true) +bool(true) +E +bool(true) diff --git a/Zend/tests/generics/runtime/lsb_through_monomorph.phpt b/Zend/tests/generics/runtime/lsb_through_monomorph.phpt index 39b0d2c31e5b..259953f81097 100644 --- a/Zend/tests/generics/runtime/lsb_through_monomorph.phpt +++ b/Zend/tests/generics/runtime/lsb_through_monomorph.phpt @@ -27,7 +27,10 @@ var_dump($intCls::whoAmI()); var_dump($strCls::whoAmI()); var_dump(StrBox::whoAmI()); -// static::$prop sees the late-bound class's storage. +// static::$prop sees the late-bound class's storage. Monomorphs share the +// base's static property (Box and Box are one slot); StrBox +// redeclares $count, so it gets its own — exactly as a subclass does in +// stock PHP. $intCls::bump(); $intCls::bump(); $strCls::bump(); @@ -50,8 +53,8 @@ var_dump($b::class); string(8) "Box" string(11) "Box" string(6) "StrBox" -int(2) -int(1) +int(3) +int(3) int(101) string(3) "Box" string(6) "StrBox" diff --git a/Zend/tests/generics/runtime/naked_new_no_defaults_self_vs_byname.phpt b/Zend/tests/generics/runtime/naked_new_no_defaults_self_vs_byname.phpt new file mode 100644 index 000000000000..fcbfe40d7ca4 --- /dev/null +++ b/Zend/tests/generics/runtime/naked_new_no_defaults_self_vs_byname.phpt @@ -0,0 +1,42 @@ +--TEST-- +Naked new in a no-defaults generic class: `new self()` binds from the frame, by-name `new C()` is an error +--FILE-- +()`. +final readonly class Box<+T> { + public function __construct(public T $value) {} + public function cloneSelf(): self { return new self($this->value); } + public function cloneStatic(): static { return new static($this->value); } + public function cloneByName(): Box { return new Box($this->value); } +} + +$b = new Box::(42); + +// `new self()` -> the live monomorph, not a bare Box. +$s = $b->cloneSelf(); +var_dump($s::class); +var_dump($s->value); + +// `new static()` -> the called scope's monomorph. +$t = $b->cloneStatic(); +var_dump($t::class); + +// `new Box()` by name (lexical self-reference) -> ambiguous type args -> a +// catchable error at runtime. (An external by-name `new Box()` the compiler can +// see is rejected at compile time instead; see naked_new_without_defaults.phpt.) +try { + $b->cloneByName(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +string(8) "Box" +int(42) +string(8) "Box" +Cannot instantiate generic class Box without type arguments; type parameter T has no default diff --git a/Zend/tests/generics/runtime/naked_new_self_no_defaults_lenient.phpt b/Zend/tests/generics/runtime/naked_new_self_no_defaults_lenient.phpt deleted file mode 100644 index 45be1e78404d..000000000000 --- a/Zend/tests/generics/runtime/naked_new_self_no_defaults_lenient.phpt +++ /dev/null @@ -1,27 +0,0 @@ ---TEST-- -Lexical `new self()` and `new ThisClass()` in a no-defaults generic class fall back to a bare instance ---FILE-- - with no default doesn't error — it creates a -// bare Box instance, preserving the lexical-self semantic where T is in scope -// but can't be statically bound to a concrete type at the call site. -final readonly class Box<+T> { - public function __construct(public T $value) {} - public function cloneSelf(): self { return new self($this->value); } - public function cloneByName(): Box { return new Box($this->value); } -} - -$b = new Box::(42); -$c1 = $b->cloneSelf(); -$c2 = $b->cloneByName(); - -var_dump($c1::class); -var_dump($c2::class); -var_dump($c1->value); -var_dump($c2->value); -?> ---EXPECT-- -string(3) "Box" -string(3) "Box" -int(42) -int(42) diff --git a/Zend/tests/generics/runtime/static_property_isolation.phpt b/Zend/tests/generics/runtime/static_property_isolation.phpt deleted file mode 100644 index 8c40c14b3a25..000000000000 --- a/Zend/tests/generics/runtime/static_property_isolation.phpt +++ /dev/null @@ -1,47 +0,0 @@ ---TEST-- -Monomorph isolation: each synthesized monomorph has its own static-property storage ---FILE-- - { - public static int $count = 0; - public static array $items = []; -} - -// Force synthesis of two distinct monomorphs. -new Counter::(); -new Counter::(); - -$intCls = "Counter"; -$strCls = "Counter"; - -$intCls::$count = 5; -$strCls::$count = 10; - -$intCls::$items[] = "int-a"; -$intCls::$items[] = "int-b"; -$strCls::$items[] = "string-only"; - -var_dump($intCls::$count); -var_dump($strCls::$count); -var_dump(Counter::$count); - -var_dump($intCls::$items); -var_dump($strCls::$items); -var_dump(Counter::$items); -?> ---EXPECT-- -int(5) -int(10) -int(0) -array(2) { - [0]=> - string(5) "int-a" - [1]=> - string(5) "int-b" -} -array(1) { - [0]=> - string(11) "string-only" -} -array(0) { -} diff --git a/Zend/tests/generics/runtime/static_property_shared.phpt b/Zend/tests/generics/runtime/static_property_shared.phpt new file mode 100644 index 000000000000..3408f806a486 --- /dev/null +++ b/Zend/tests/generics/runtime/static_property_shared.phpt @@ -0,0 +1,51 @@ +--TEST-- +Static properties are shared between a generic base and all of its monomorphs +--FILE-- + { + public static int $count = 0; + public static array $items = []; + public static function bump(): void { static::$count++; } +} + +new Counter::(); +new Counter::(); + +$intCls = "Counter"; +$strCls = "Counter"; + +// Writes through any view land in the same storage. +$intCls::$count = 5; +$strCls::$count = 10; // overwrites the shared slot +var_dump($intCls::$count, $strCls::$count, Counter::$count); + +// Appends accumulate into one array regardless of the view used. +$intCls::$items[] = "via-int"; +$strCls::$items[] = "via-string"; +Counter::$items[] = "via-base"; +var_dump(Counter::$items); + +// LSB `static::` from a monomorph instance reaches the same storage as the base. +$c = new Counter::(); +$c::bump(); // static = Counter -> shared slot (now 11) +Counter::bump(); // base -> same slot (now 12) +var_dump($c::$count, Counter::$count); +?> +--EXPECT-- +int(10) +int(10) +int(10) +array(3) { + [0]=> + string(7) "via-int" + [1]=> + string(10) "via-string" + [2]=> + string(8) "via-base" +} +int(12) +int(12) diff --git a/Zend/tests/generics/scoping/trait_use_clause.phpt b/Zend/tests/generics/scoping/trait_use_clause.phpt index 9c50c7a98a53..6a72ab293df9 100644 --- a/Zend/tests/generics/scoping/trait_use_clause.phpt +++ b/Zend/tests/generics/scoping/trait_use_clause.phpt @@ -14,8 +14,9 @@ class Box { // original unbounded "mixed". echo (new ReflectionClass('Box'))->getMethod('tag')->getReturnType()->getName(), "\n"; -// On a bare (unmonomorphed) Box, the runtime check enforces the bound. -$b = new Box; +// On a monomorph Box, the trait method's X is substituted to the +// concrete argument and the runtime check enforces it. +$b = new Box::(); echo get_class($b), "\n"; var_dump($b->tag(new stdClass) instanceof stdClass); @@ -27,6 +28,6 @@ try { ?> --EXPECTF-- object -Box +Box bool(true) -rejected: Box::tag(): Argument #1 ($x) must be of type object, int given%S +rejected: Box::tag(): Argument #1 ($x) must be of type stdClass, int given%S diff --git a/Zend/tests/generics/traits/trait_use_args_compile.phpt b/Zend/tests/generics/traits/trait_use_args_compile.phpt index 84ae43ab0ff0..84af6901238c 100644 --- a/Zend/tests/generics/traits/trait_use_args_compile.phpt +++ b/Zend/tests/generics/traits/trait_use_args_compile.phpt @@ -8,10 +8,10 @@ trait Holder { class Box { use Holder; } -$b = new Box; +$b = new Box::(); echo get_class($b), "\n"; echo "ok\n"; ?> --EXPECT-- -Box +Box ok diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index db64af028da0..34c71dba92d1 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -6901,6 +6901,23 @@ ZEND_API zend_class_entry *zend_get_defaults_monomorph(zend_class_entry *base) ZEND_UNREACHABLE(); } +/* For a naked `new self()` / `new ThisClass()` that lexically names the generic + * class `lexical`, return the monomorph carrying the current frame's class-level + * binding: walk from the called scope up to the monomorph whose base is + * `lexical` (the same walk zend_resolve_generic_type_param uses to find a + * class-level T-ref's binding). Returns NULL when no such binding is in scope + * (e.g. a static call on the bare generic itself), so the caller can fall back + * to defaults or raise an error. */ +ZEND_API zend_class_entry *zend_resolve_lexical_self_monomorph( + zend_class_entry *lexical, const zend_execute_data *ex) +{ + zend_class_entry *cur = ex ? zend_get_called_scope(ex) : NULL; + while (cur && cur->parent != lexical) { + cur = cur->parent; + } + return (cur && cur->parent == lexical) ? cur : NULL; +} + /* When `new C::<...>(...)` is compiled inside a generic function/class, the * turbofish args may reference enclosing-scope T parameters by ref. The op_array * side-table stores those refs verbatim — at synth time they must be resolved @@ -7162,25 +7179,6 @@ ZEND_API zend_class_entry *zend_synthesize_monomorph( return NULL; } - /* Isolate static-property storage from the base class. Inheritance normally - * makes the child's `default_static_members_table` slots IS_INDIRECT back to - * the parent so that all subclasses share the same live storage. For - * monomorphs that's the wrong semantic: `Box::$count` and - * `Box::$count` should be independent counters. Convert each - * INDIRECT slot to a direct copy of the underlying default; at first access - * `zend_class_init_statics` will then allocate the monomorph its own live - * slot and `ZVAL_COPY_OR_DUP` the default into it. */ - if (linked->default_static_members_count) { - for (uint32_t i = 0; i < linked->default_static_members_count; i++) { - zval *slot = &linked->default_static_members_table[i]; - if (Z_TYPE_P(slot) == IS_INDIRECT) { - zval *src = Z_INDIRECT_P(slot); - ZVAL_DEINDIRECT(src); - ZVAL_COPY_OR_DUP(slot, src); - } - } - } - /* Substitute T-typed implements on the inherited interface chain. When * the base declares `class B implements I` and is monomorphized as * `B`, the inherited interfaces[] still points at the erased diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index 86f1460bbba0..66023d8708a1 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -86,6 +86,13 @@ ZEND_API zend_class_entry *zend_synthesize_monomorph_resolved( * `new static()` and dynamic `new $name()` paths. */ ZEND_API zend_class_entry *zend_get_defaults_monomorph(zend_class_entry *base); +/* Resolve a naked `new self()` / lexical `new ThisClass()` to the monomorph that + * carries the executing frame's class-level binding (e.g. `new self()` in a + * `C` instance method -> `C`). Returns NULL when no binding is in + * scope. See the definition in zend_inheritance.c. */ +ZEND_API zend_class_entry *zend_resolve_lexical_self_monomorph( + zend_class_entry *lexical, const zend_execute_data *ex); + /* True when the name has monomorph-canonical shape (contains `<`). The canonical * encoding for synthesized monomorphs embeds `<...>` in the class name, which is * invalid in any user-declared class. Use these helpers rather than open-coding diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index e2d997dd099f..52bce47d103e 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6112,20 +6112,32 @@ ZEND_VM_HANDLER(68, ZEND_NEW, UNUSED|CLASS_FETCH|CONST|VAR, UNUSED|CACHE_SLOT, N ce = Z_CE_P(EX_VAR(opline->op1.var)); } - /* Naked `new` of a bare generic class (via `new static()`, `new $name`, - * or any path that didn't go through compile-time canonical-name rewrite). - * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles - * the synthesis-and-swap for the turbofish path. Otherwise: if every - * type parameter has a default, synthesize and use the defaults monomorph. - * For `new static()` / lexical paths with no defaults, fall back to a - * bare instance (preserves the lexical-self semantic for generic classes - * whose authors didn't declare defaults). For dynamic `new $name()` we - * throw — the caller spelled out a generic class by name and a bare - * instance with erased T is almost never what they want. + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. * - * OP1_TYPE distinguishes: IS_UNUSED → static/self/parent (lenient); - * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ - if (UNEXPECTED(OP1_TYPE != IS_CONST && ce->generic_parameters + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { ce = zend_synthesize_monomorph(ce, NULL, 0); @@ -6133,15 +6145,33 @@ ZEND_VM_HANDLER(68, ZEND_NEW, UNUSED|CLASS_FETCH|CONST|VAR, UNUSED|CACHE_SLOT, N ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } - } else if (OP1_TYPE == IS_VAR) { - zend_throw_error(NULL, - "Cannot instantiate generic class %s without type arguments " - "via dynamic class name; no defaults declared", - ZSTR_VAL(ce->name)); - ZVAL_UNDEF(EX_VAR(opline->result.var)); - HANDLE_EXCEPTION(); + if (OP1_TYPE == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (OP1_TYPE == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (OP1_TYPE == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } } - /* IS_UNUSED with no defaults: fall through with the bare ce. */ } result = EX_VAR(opline->result.var); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index cb9d408e76c1..581b3e8f2a2b 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -11566,20 +11566,32 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_CONS ce = Z_CE_P(EX_VAR(opline->op1.var)); } - /* Naked `new` of a bare generic class (via `new static()`, `new $name`, - * or any path that didn't go through compile-time canonical-name rewrite). - * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles - * the synthesis-and-swap for the turbofish path. Otherwise: if every - * type parameter has a default, synthesize and use the defaults monomorph. - * For `new static()` / lexical paths with no defaults, fall back to a - * bare instance (preserves the lexical-self semantic for generic classes - * whose authors didn't declare defaults). For dynamic `new $name()` we - * throw — the caller spelled out a generic class by name and a bare - * instance with erased T is almost never what they want. + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. * - * IS_CONST distinguishes: IS_UNUSED → static/self/parent (lenient); - * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ - if (UNEXPECTED(IS_CONST != IS_CONST && ce->generic_parameters + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { ce = zend_synthesize_monomorph(ce, NULL, 0); @@ -11587,15 +11599,33 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_CONS ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } - } else if (IS_CONST == IS_VAR) { - zend_throw_error(NULL, - "Cannot instantiate generic class %s without type arguments " - "via dynamic class name; no defaults declared", - ZSTR_VAL(ce->name)); - ZVAL_UNDEF(EX_VAR(opline->result.var)); - HANDLE_EXCEPTION(); + if (IS_CONST == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_CONST == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_CONST == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } } - /* IS_UNUSED with no defaults: fall through with the bare ce. */ } result = EX_VAR(opline->result.var); @@ -30617,20 +30647,32 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_VAR_ ce = Z_CE_P(EX_VAR(opline->op1.var)); } - /* Naked `new` of a bare generic class (via `new static()`, `new $name`, - * or any path that didn't go through compile-time canonical-name rewrite). - * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles - * the synthesis-and-swap for the turbofish path. Otherwise: if every - * type parameter has a default, synthesize and use the defaults monomorph. - * For `new static()` / lexical paths with no defaults, fall back to a - * bare instance (preserves the lexical-self semantic for generic classes - * whose authors didn't declare defaults). For dynamic `new $name()` we - * throw — the caller spelled out a generic class by name and a bare - * instance with erased T is almost never what they want. + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. * - * IS_VAR distinguishes: IS_UNUSED → static/self/parent (lenient); - * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ - if (UNEXPECTED(IS_VAR != IS_CONST && ce->generic_parameters + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { ce = zend_synthesize_monomorph(ce, NULL, 0); @@ -30638,15 +30680,33 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_VAR_ ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } - } else if (IS_VAR == IS_VAR) { - zend_throw_error(NULL, - "Cannot instantiate generic class %s without type arguments " - "via dynamic class name; no defaults declared", - ZSTR_VAL(ce->name)); - ZVAL_UNDEF(EX_VAR(opline->result.var)); - HANDLE_EXCEPTION(); + if (IS_VAR == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_VAR == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_VAR == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } } - /* IS_UNUSED with no defaults: fall through with the bare ce. */ } result = EX_VAR(opline->result.var); @@ -37797,20 +37857,32 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_UNUS ce = Z_CE_P(EX_VAR(opline->op1.var)); } - /* Naked `new` of a bare generic class (via `new static()`, `new $name`, - * or any path that didn't go through compile-time canonical-name rewrite). - * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles - * the synthesis-and-swap for the turbofish path. Otherwise: if every - * type parameter has a default, synthesize and use the defaults monomorph. - * For `new static()` / lexical paths with no defaults, fall back to a - * bare instance (preserves the lexical-self semantic for generic classes - * whose authors didn't declare defaults). For dynamic `new $name()` we - * throw — the caller spelled out a generic class by name and a bare - * instance with erased T is almost never what they want. + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. * - * IS_UNUSED distinguishes: IS_UNUSED → static/self/parent (lenient); - * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ - if (UNEXPECTED(IS_UNUSED != IS_CONST && ce->generic_parameters + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { ce = zend_synthesize_monomorph(ce, NULL, 0); @@ -37818,15 +37890,33 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_UNUS ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } - } else if (IS_UNUSED == IS_VAR) { - zend_throw_error(NULL, - "Cannot instantiate generic class %s without type arguments " - "via dynamic class name; no defaults declared", - ZSTR_VAL(ce->name)); - ZVAL_UNDEF(EX_VAR(opline->result.var)); - HANDLE_EXCEPTION(); + if (IS_UNUSED == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_UNUSED == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_UNUSED == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } } - /* IS_UNUSED with no defaults: fall through with the bare ce. */ } result = EX_VAR(opline->result.var); @@ -65180,20 +65270,32 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_CONST_UNU ce = Z_CE_P(EX_VAR(opline->op1.var)); } - /* Naked `new` of a bare generic class (via `new static()`, `new $name`, - * or any path that didn't go through compile-time canonical-name rewrite). - * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles - * the synthesis-and-swap for the turbofish path. Otherwise: if every - * type parameter has a default, synthesize and use the defaults monomorph. - * For `new static()` / lexical paths with no defaults, fall back to a - * bare instance (preserves the lexical-self semantic for generic classes - * whose authors didn't declare defaults). For dynamic `new $name()` we - * throw — the caller spelled out a generic class by name and a bare - * instance with erased T is almost never what they want. + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. * - * IS_CONST distinguishes: IS_UNUSED → static/self/parent (lenient); - * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ - if (UNEXPECTED(IS_CONST != IS_CONST && ce->generic_parameters + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { ce = zend_synthesize_monomorph(ce, NULL, 0); @@ -65201,15 +65303,33 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_CONST_UNU ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } - } else if (IS_CONST == IS_VAR) { - zend_throw_error(NULL, - "Cannot instantiate generic class %s without type arguments " - "via dynamic class name; no defaults declared", - ZSTR_VAL(ce->name)); - ZVAL_UNDEF(EX_VAR(opline->result.var)); - HANDLE_EXCEPTION(); + if (IS_CONST == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_CONST == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_CONST == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } } - /* IS_UNUSED with no defaults: fall through with the bare ce. */ } result = EX_VAR(opline->result.var); @@ -84131,20 +84251,32 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_VAR_UNUSE ce = Z_CE_P(EX_VAR(opline->op1.var)); } - /* Naked `new` of a bare generic class (via `new static()`, `new $name`, - * or any path that didn't go through compile-time canonical-name rewrite). - * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles - * the synthesis-and-swap for the turbofish path. Otherwise: if every - * type parameter has a default, synthesize and use the defaults monomorph. - * For `new static()` / lexical paths with no defaults, fall back to a - * bare instance (preserves the lexical-self semantic for generic classes - * whose authors didn't declare defaults). For dynamic `new $name()` we - * throw — the caller spelled out a generic class by name and a bare - * instance with erased T is almost never what they want. + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. * - * IS_VAR distinguishes: IS_UNUSED → static/self/parent (lenient); - * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ - if (UNEXPECTED(IS_VAR != IS_CONST && ce->generic_parameters + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { ce = zend_synthesize_monomorph(ce, NULL, 0); @@ -84152,15 +84284,33 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_VAR_UNUSE ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } - } else if (IS_VAR == IS_VAR) { - zend_throw_error(NULL, - "Cannot instantiate generic class %s without type arguments " - "via dynamic class name; no defaults declared", - ZSTR_VAL(ce->name)); - ZVAL_UNDEF(EX_VAR(opline->result.var)); - HANDLE_EXCEPTION(); + if (IS_VAR == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_VAR == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_VAR == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } } - /* IS_UNUSED with no defaults: fall through with the bare ce. */ } result = EX_VAR(opline->result.var); @@ -91311,20 +91461,32 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_UNUSED_UN ce = Z_CE_P(EX_VAR(opline->op1.var)); } - /* Naked `new` of a bare generic class (via `new static()`, `new $name`, - * or any path that didn't go through compile-time canonical-name rewrite). - * Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode handles - * the synthesis-and-swap for the turbofish path. Otherwise: if every - * type parameter has a default, synthesize and use the defaults monomorph. - * For `new static()` / lexical paths with no defaults, fall back to a - * bare instance (preserves the lexical-self semantic for generic classes - * whose authors didn't declare defaults). For dynamic `new $name()` we - * throw — the caller spelled out a generic class by name and a bare - * instance with erased T is almost never what they want. + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. * - * IS_UNUSED distinguishes: IS_UNUSED → static/self/parent (lenient); - * IS_VAR → dynamic name resolved via FETCH_CLASS (strict). */ - if (UNEXPECTED(IS_UNUSED != IS_CONST && ce->generic_parameters + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { ce = zend_synthesize_monomorph(ce, NULL, 0); @@ -91332,15 +91494,33 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_UNUSED_UN ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } - } else if (IS_UNUSED == IS_VAR) { - zend_throw_error(NULL, - "Cannot instantiate generic class %s without type arguments " - "via dynamic class name; no defaults declared", - ZSTR_VAL(ce->name)); - ZVAL_UNDEF(EX_VAR(opline->result.var)); - HANDLE_EXCEPTION(); + if (IS_UNUSED == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_UNUSED == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_UNUSED == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } } - /* IS_UNUSED with no defaults: fall through with the bare ce. */ } result = EX_VAR(opline->result.var); From 11e74939bd1524682cf52dc1db69b5663df89062 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 19 Jun 2026 12:16:17 +0200 Subject: [PATCH 26/30] fold union/intersection with return types Signed-off-by: Robert Landers --- .../errors/intersection_t_bound_scalar.phpt | 2 +- .../errors/intersection_t_unbounded.phpt | 2 +- .../composite_return_reification.phpt | 58 ++++++++ Zend/zend_compile.c | 16 ++- Zend/zend_inheritance.c | 129 ++++++++++++++++-- Zend/zend_inheritance.h | 2 + 6 files changed, 189 insertions(+), 20 deletions(-) create mode 100644 Zend/tests/generics/reification/composite_return_reification.phpt diff --git a/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt b/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt index 4ce03e67f0dd..a0deb98dca56 100644 --- a/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt +++ b/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt @@ -6,4 +6,4 @@ class Foo {} function x(): T & Foo {} ?> --EXPECTF-- -Fatal error: Type parameter T with bound string cannot be part of an intersection type; use an object-shaped bound (e.g. T: object) in %s on line %d +Fatal error: Type parameter T with bound string cannot be part of an intersection type; bound it to a class or interface (e.g. T: SomeInterface) in %s on line %d diff --git a/Zend/tests/generics/errors/intersection_t_unbounded.phpt b/Zend/tests/generics/errors/intersection_t_unbounded.phpt index 940b218d2e5c..6fe0e50df2c7 100644 --- a/Zend/tests/generics/errors/intersection_t_unbounded.phpt +++ b/Zend/tests/generics/errors/intersection_t_unbounded.phpt @@ -6,4 +6,4 @@ class Foo {} function x(): T & Foo {} ?> --EXPECTF-- -Fatal error: Type parameter T with bound mixed cannot be part of an intersection type; use an object-shaped bound (e.g. T: object) in %s on line %d +Fatal error: Type parameter T with bound mixed cannot be part of an intersection type; bound it to a class or interface (e.g. T: SomeInterface) in %s on line %d diff --git a/Zend/tests/generics/reification/composite_return_reification.phpt b/Zend/tests/generics/reification/composite_return_reification.phpt new file mode 100644 index 000000000000..1bd93c75b1ea --- /dev/null +++ b/Zend/tests/generics/reification/composite_return_reification.phpt @@ -0,0 +1,58 @@ +--TEST-- +Reification: composite return/param types (T|Other, A|B, T&B) are reified and enforced, with null/member folding +--FILE-- +(mixed $x): T|Other { return $x; } +var_dump(ret::(new Foo)::class); // Foo member +var_dump(ret::(new Other)::class); // Other member +try { ret::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// When T resolves to the other member the union folds (Other|Other -> Other). +try { ret::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// A union *binding* flattens into the union position (Foo|Other)|Other -> Foo|Other. +var_dump(ret::(new Foo)::class); +try { ret::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// A union of two type parameters. +function two(mixed $x): X|Y { return $x; } +var_dump(two::(new Other)::class); +try { two::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// Composite parameter types are enforced too. +function par(T|Other $x): string { return $x::class; } +var_dump(par::(new Foo)); +try { par::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// Intersection return: T bound to an interface, T&B reifies to A&B. +function inter(mixed $x): T&B { return $x; } +var_dump(inter::(new AB)::class); +try { inter::(new Foo); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// DNF: an intersection binding spliced into a union stays nested ((A&B)|Other). +function dnf(mixed $x): T|Other { return $x; } +var_dump(dnf::(new AB)::class); +var_dump(dnf::(new Other)::class); +try { dnf::(new Foo); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECTF-- +string(3) "Foo" +string(5) "Other" +ret(): Return value must be of type Foo|Other, int returned +ret(): Return value must be of type Other, int returned +string(3) "Foo" +ret(): Return value must be of type Foo|Other, int returned +string(5) "Other" +two(): Return value must be of type Foo|Other, int returned +string(3) "Foo" +par(): Argument #1 ($x) must be of type Foo|Other, int given, called in %s on line %d +string(2) "AB" +inter(): Return value must be of type A&B, Foo returned +string(2) "AB" +string(5) "Other" +dnf(): Return value must be of type (A&B)|Other, Foo returned diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index cc05ff8c9478..04a5d978534f 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -4801,14 +4801,18 @@ static void zend_emit_return_type_check( } } - /* Generic functions: if the return is a T-ref, the runtime opcode - * is the only place the reified binding gets checked. Detect that - * here so the two elision paths below don't drop the opcode. */ + /* Generic functions: if the return references a type parameter, the + * runtime opcode is the only place the reified binding gets checked. + * Detect that here so the two elision paths below don't drop the opcode. + * Besides a bare `T`, a leaf union/intersection (`T|Other`, `A|B`) also + * reifies (see zend_monomorph_build_arg_info); a `Box`-style + * NAMED_WITH_ARGS composite stays erased, so it needs no opcode. */ zend_op_array *active = CG(active_op_array); const zend_type *pre_return = active->generic_types ? active->generic_types->return_type : NULL; bool return_is_generic = pre_return - && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_return); + && (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_return) + || zend_type_is_reifiable_leaf_composite(*pre_return)); if (expr && ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY) { /* Mixed normally needs no run-time check, but if the return is a @@ -10350,7 +10354,7 @@ static zend_type zend_compile_typename_ex( if (t_param) { zend_error_noreturn(E_COMPILE_ERROR, "Type parameter %s with bound %s cannot be part of an intersection type; " - "use an object-shaped bound (e.g. %s: object)", + "bound it to a class or interface (e.g. %s: SomeInterface)", ZSTR_VAL(t_param->name), ZSTR_VAL(standard_type_str), ZSTR_VAL(t_param->name)); } @@ -10363,7 +10367,7 @@ static zend_type zend_compile_typename_ex( if (t_param) { zend_error_noreturn(E_COMPILE_ERROR, "Type parameter %s with bound %s cannot be part of an intersection type; " - "use an object-shaped bound (e.g. %s: object)", + "bound it to a class or interface (e.g. %s: SomeInterface)", ZSTR_VAL(t_param->name), ZSTR_VAL(standard_type_str), ZSTR_VAL(t_param->name)); } diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 34c71dba92d1..a961d8b4a2e9 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -881,6 +881,65 @@ static bool zend_get_inheritance_binding( return false; } +/* True when `t` carries a NAMED_WITH_ARGS payload (`Box`) anywhere inside it. + * Building block for zend_type_is_reifiable_leaf_composite below. */ +static bool zend_type_contains_named_with_args(zend_type t) +{ + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(t)) return true; + if (ZEND_TYPE_HAS_LIST(t)) { + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(t), member) { + if (zend_type_contains_named_with_args(*member)) return true; + } ZEND_TYPE_LIST_FOREACH_END(); + } + return false; +} + +/* A union/intersection whose leaves include a type parameter but which carries + * no `Box`-style NAMED_WITH_ARGS reifies to a concrete erased-model shape + * (`T|Other` -> `Foo|Other`). An NWA composite stays erased — folding its + * monomorph name into a check would reject the plain instances a body emits, so + * callers leave it alone. A bare top-level T-ref is handled separately by + * callers (they substitute the leaf directly). Shared by the return-opcode + * elision (zend_emit_return_type_check) and the monomorph arg_info builder so + * the two decisions can't drift. */ +ZEND_API bool zend_type_is_reifiable_leaf_composite(zend_type t) +{ + return ZEND_TYPE_HAS_LIST(t) + && zend_type_contains_type_parameter(t) + && !zend_type_contains_named_with_args(t); +} + +/* Two union members are "the same" for dedup purposes when they name the same + * class (case-insensitively) or refer to the same type parameter. Substituting a + * binding that already appears as a sibling member (`T|Other` with `T = Other`) + * would otherwise leave a redundant `Other|Other` in the list. */ +static bool zend_union_member_equals(zend_type a, zend_type b) +{ + if (ZEND_TYPE_HAS_NAME(a) && ZEND_TYPE_HAS_NAME(b)) { + return zend_string_equals_ci(ZEND_TYPE_NAME(a), ZEND_TYPE_NAME(b)); + } + if (ZEND_TYPE_HAS_TYPE_PARAMETER(a) && ZEND_TYPE_HAS_TYPE_PARAMETER(b)) { + const zend_type_parameter_ref *ra = ZEND_TYPE_TYPE_PARAMETER(a); + const zend_type_parameter_ref *rb = ZEND_TYPE_TYPE_PARAMETER(b); + return ra->origin == rb->origin && ra->index == rb->index; + } + return false; +} + +/* Append `member` to a union-build buffer unless an equal member (per + * zend_union_member_equals) is already present. */ +static zend_always_inline void zend_union_push_unique( + zend_type *out, uint32_t *out_count, zend_type member) +{ + for (uint32_t j = 0; j < *out_count; j++) { + if (zend_union_member_equals(out[j], member)) { + return; + } + } + out[(*out_count)++] = member; +} + /* Substitutes class-scope T-refs with their bound arguments. * * Handles three shapes: @@ -1024,16 +1083,48 @@ static zend_type zend_substitute_leaf_type_param_origin(zend_type t, const zend_ & ~_ZEND_TYPE_UNION_BIT & ~_ZEND_TYPE_INTERSECTION_BIT; uint32_t merged_mask = carried_flags; + /* Substitute every member up front, then size the output: a union binding + * spliced into a union flattens (PHP unions can't nest), so reserve room for + * each of its members rather than one slot. */ + ALLOCA_FLAG(sub_heap) + zend_type *subbed = (zend_type *) do_alloca(sizeof(zend_type) * src_list->num_types, sub_heap); + uint32_t cap = 0; + for (uint32_t i = 0; i < src_list->num_types; i++) { + subbed[i] = zend_substitute_leaf_type_param_origin(src_list->types[i], args, arity, origin); + if (!is_intersection && ZEND_TYPE_HAS_LIST(subbed[i]) + && !ZEND_TYPE_IS_INTERSECTION(subbed[i])) { + cap += ZEND_TYPE_LIST(subbed[i])->num_types; + } else { + cap += 1; + } + } + ALLOCA_FLAG(use_heap) - zend_type *out = (zend_type *) do_alloca(sizeof(zend_type) * src_list->num_types, use_heap); + zend_type *out = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); uint32_t out_count = 0; for (uint32_t i = 0; i < src_list->num_types; i++) { - zend_type substituted = zend_substitute_leaf_type_param_origin(src_list->types[i], args, arity, origin); + zend_type substituted = subbed[i]; + /* The substituted scalar contribution is OR'd into the outer mask in case + * it carries a NULLABLE bit (or, for a union binding, member scalars). */ + merged_mask |= ZEND_TYPE_PURE_MASK(substituted); + + /* Flatten a union binding spliced into a union: (Foo|Other)|Other + * collapses to Foo|Other|Other (then dedupes). A nested union member + * would otherwise reach the runtime union check, which only expects + * plain or intersection members. */ + bool flatten = !is_intersection && ZEND_TYPE_HAS_LIST(substituted) + && !ZEND_TYPE_IS_INTERSECTION(substituted); + if (flatten) { + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(substituted), member) { + zend_union_push_unique(out, &out_count, *member); + } ZEND_TYPE_LIST_FOREACH_END(); + continue; + } /* Keep complex elements (named types, intersection sublists, unresolved - * T-refs) in the list; their scalar contribution is also OR'd into the - * outer mask in case the substituted type carries a NULLABLE bit. */ + * T-refs) in the list. */ bool keeps_complex = ZEND_TYPE_HAS_LIST(substituted) || ZEND_TYPE_HAS_NAME(substituted) || ZEND_TYPE_HAS_LITERAL_NAME(substituted) @@ -1041,10 +1132,10 @@ static zend_type zend_substitute_leaf_type_param_origin(zend_type t, const zend_ || ZEND_TYPE_HAS_NAMED_WITH_ARGS(substituted); if (keeps_complex) { - out[out_count++] = substituted; + zend_union_push_unique(out, &out_count, substituted); } - merged_mask |= ZEND_TYPE_PURE_MASK(substituted); } + free_alloca(subbed, sub_heap); zend_type result; if (out_count == 0) { @@ -7709,15 +7800,29 @@ static zend_arg_info *zend_monomorph_build_arg_info( pre = zv ? (const zend_type *) Z_PTR_P(zv) : NULL; } - /* Only specialize a BARE FUNCTION_LIKE type-parameter leaf (`T $x`). - * Composite generic types keep the base's erased arg_info: the erased model - * represents generic instances by their plain class, so folding to a - * monomorph name would make RECV reject the plain instances the body emits. */ + /* Specialize a BARE FUNCTION_LIKE type-parameter leaf (`T $x`), and also a + * union/intersection of leaves (`T|Other`, `A|B`): substitute the function + * T-refs and, when every member is then ground, install the concrete type so + * RECV/return checks enforce it. A composite carrying a `Box`-style + * NAMED_WITH_ARGS stays erased — the erased model represents generic instances + * by their plain class, so folding to a monomorph name would make RECV reject + * the plain instances the body emits. */ bool is_bare_leaf = pre && ZEND_TYPE_IS_SET(*pre) && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre) && ZEND_TYPE_TYPE_PARAMETER(*pre)->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE; - if (is_bare_leaf) { - zend_type sub = zend_substitute_function_type_param(*pre, args, arity); + bool is_leaf_union = !is_bare_leaf && pre && ZEND_TYPE_IS_SET(*pre) + && zend_type_is_reifiable_leaf_composite(*pre); + zend_type sub; + bool substitute = is_bare_leaf || is_leaf_union; + if (substitute) { + sub = zend_substitute_function_type_param(*pre, args, arity); + /* A composite that didn't fully ground (an unbound T remains) keeps + * the erased arg_info; a bare leaf always grounds here. */ + if (is_leaf_union && zend_type_contains_type_parameter(sub)) { + substitute = false; + } + } + if (substitute) { zend_type_copy_ctor(&sub, /* use_arena */ true, /* persistent */ false); new_block[slot].type = sub; } else { diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index 66023d8708a1..3e5c6f313146 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -118,6 +118,8 @@ ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( ZEND_API zend_type zend_substitute_function_type_param(zend_type t, const zend_type *args, uint32_t arity); +ZEND_API bool zend_type_is_reifiable_leaf_composite(zend_type t); + /* Synthesize (or return the cached) concrete specialization of generic function * `base` for the given type args, registered in EG(function_table) as * `base`. Returns NULL when args are not concrete or base isn't generic. */ From 5e28858af8a78ba7151ee1c696f132bce87cac9e Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 20 Jun 2026 00:46:33 +0200 Subject: [PATCH 27/30] handle inference for scalar values Signed-off-by: Robert Landers --- .../composite_return_reification.phpt | 9 +++++++ Zend/zend_compile.c | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Zend/tests/generics/reification/composite_return_reification.phpt b/Zend/tests/generics/reification/composite_return_reification.phpt index 1bd93c75b1ea..c6e48ee04c36 100644 --- a/Zend/tests/generics/reification/composite_return_reification.phpt +++ b/Zend/tests/generics/reification/composite_return_reification.phpt @@ -29,6 +29,13 @@ function par(T|Other $x): string { return $x::class; } var_dump(par::(new Foo)); try { par::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } +// Parameters fold/flatten through the same path as returns: T=Other folds the +// duplicate, and a union binding flattens. The reified param type is observable. +par::(new Other); +var_dump((string) (new ReflectionFunction('par'))->getParameters()[0]->getType()); +par::(new Foo); +var_dump((string) (new ReflectionFunction('par'))->getParameters()[0]->getType()); + // Intersection return: T bound to an interface, T&B reifies to A&B. function inter(mixed $x): T&B { return $x; } var_dump(inter::(new AB)::class); @@ -51,6 +58,8 @@ string(5) "Other" two(): Return value must be of type Foo|Other, int returned string(3) "Foo" par(): Argument #1 ($x) must be of type Foo|Other, int given, called in %s on line %d +string(5) "Other" +string(9) "Foo|Other" string(2) "AB" inter(): Return value must be of type A&B, Foo returned string(2) "AB" diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 04a5d978534f..4e0b39bd2c35 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1165,6 +1165,30 @@ ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( zend_string *inferred_name = zend_string_copy(arg_ce->name); table->entries[ref->index].owned_type = (zend_type) ZEND_TYPE_INIT_CLASS(inferred_name, 0, 0); + } else { + /* Scalar/array values infer to their builtin type. Map the + * zval type to a type code, synthesise a mask owned_type, and + * take its canonical name ("int", "string", ...) so reflection + * and a reified `: T` return enforce the inferred type instead + * of collapsing to the default. IS_TRUE/IS_FALSE both map to + * bool; IS_NULL and other non-reifiable kinds (resources) are + * left for the declared default. */ + uint32_t type_mask = 0; + switch (Z_TYPE_P(arg)) { + case IS_LONG: type_mask = MAY_BE_LONG; break; + case IS_DOUBLE: type_mask = MAY_BE_DOUBLE; break; + case IS_STRING: type_mask = MAY_BE_STRING; break; + case IS_ARRAY: type_mask = MAY_BE_ARRAY; break; + case IS_TRUE: + case IS_FALSE: type_mask = MAY_BE_BOOL; break; + default: break; + } + if (type_mask) { + zend_type inferred = (zend_type) ZEND_TYPE_INIT_MASK(type_mask); + table->entries[ref->index].name = + zend_type_arg_canonical_name(inferred); + table->entries[ref->index].owned_type = inferred; + } } } ZEND_HASH_FOREACH_END(); } From 9076fd17cd818602fd9eb16b7e70d172c6cd7659 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 20 Jun 2026 01:53:48 +0200 Subject: [PATCH 28/30] make new static::<> a compile time error Signed-off-by: Robert Landers --- .../errors/type_args_on_static_new.phpt | 18 ++++++++ .../type_args_on_static_new_subclass.phpt | 17 +++++++ .../reification/inference_scalar.phpt | 22 +++++++++ .../inference_scalar_return_enforced.phpt | 26 +++++++++++ .../reification/new_static_vs_self.phpt | 45 +++++++++++++++++++ Zend/zend_compile.c | 21 +++++++++ 6 files changed, 149 insertions(+) create mode 100644 Zend/tests/generics/errors/type_args_on_static_new.phpt create mode 100644 Zend/tests/generics/errors/type_args_on_static_new_subclass.phpt create mode 100644 Zend/tests/generics/reification/inference_scalar.phpt create mode 100644 Zend/tests/generics/reification/inference_scalar_return_enforced.phpt create mode 100644 Zend/tests/generics/reification/new_static_vs_self.phpt diff --git a/Zend/tests/generics/errors/type_args_on_static_new.phpt b/Zend/tests/generics/errors/type_args_on_static_new.phpt new file mode 100644 index 000000000000..2cfb0d4ae4a0 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_static_new.phpt @@ -0,0 +1,18 @@ +--TEST-- +Type arguments on `new static::<...>` are a compile-time error (static is the runtime type) +--FILE-- + { + public function __construct(private array $e = []) {} + public function dup(): static { return new static::($this->e); } +} +(new C::([1, 2]))->dup(); +?> +--EXPECTF-- +Fatal error: Type arguments cannot be applied to "static": "static" is the runtime type and already carries its type arguments. Use "new static()" to construct the exact runtime type, or "new self::<...>" to apply type arguments to the enclosing class in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_static_new_subclass.phpt b/Zend/tests/generics/errors/type_args_on_static_new_subclass.phpt new file mode 100644 index 000000000000..6c47140ea418 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_static_new_subclass.phpt @@ -0,0 +1,17 @@ +--TEST-- +`new static::<...>` is rejected even when the runtime type is a non-generic subclass of a monomorph +--FILE-- + +// but is itself non-generic — it has no type parameters to apply `` to. The +// rejection is the same compile-time scoping error: use `new static()`. +class Box { + public function __construct(private mixed $v = null) {} + public function make(): object { return new static::($this->v); } +} +class IntBox extends Box {} + +(new IntBox(5))->make(); +?> +--EXPECTF-- +Fatal error: Type arguments cannot be applied to "static": "static" is the runtime type and already carries its type arguments. Use "new static()" to construct the exact runtime type, or "new self::<...>" to apply type arguments to the enclosing class in %s on line %d diff --git a/Zend/tests/generics/reification/inference_scalar.phpt b/Zend/tests/generics/reification/inference_scalar.phpt new file mode 100644 index 000000000000..db2bc65ee52b --- /dev/null +++ b/Zend/tests/generics/reification/inference_scalar.phpt @@ -0,0 +1,22 @@ +--TEST-- +Reification: T is inferred from scalar/array arguments, not just objects +--FILE-- +(T $a): array { return func_get_type_args(); } + +var_dump(tparams(1)["T"]); +var_dump(tparams("foo")["T"]); +var_dump(tparams(1.5)["T"]); +var_dump(tparams(true)["T"]); +var_dump(tparams(false)["T"]); +var_dump(tparams([1, 2])["T"]); +var_dump(tparams(new stdClass())["T"]); +?> +--EXPECT-- +string(3) "int" +string(6) "string" +string(5) "float" +string(4) "bool" +string(4) "bool" +string(5) "array" +string(8) "stdClass" diff --git a/Zend/tests/generics/reification/inference_scalar_return_enforced.phpt b/Zend/tests/generics/reification/inference_scalar_return_enforced.phpt new file mode 100644 index 000000000000..67d3f4cba0f1 --- /dev/null +++ b/Zend/tests/generics/reification/inference_scalar_return_enforced.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: a :T return type reified from an inferred scalar T is enforced +--FILE-- +(T $a): T { return "not the same type"; } + +echo "calling bad(1) — T inferred as int, returning a string must fail\n"; +try { + var_dump(bad(1)); +} catch (TypeError $e) { + echo "TypeError: ", $e->getMessage(), "\n"; +} + +// Returning a matching type is fine. +function good(T $a): T { return $a; } +var_dump(good(42)); +var_dump(good("ok")); +?> +--EXPECT-- +calling bad(1) — T inferred as int, returning a string must fail +TypeError: bad(): Return value must be of type int, string returned +int(42) +string(2) "ok" diff --git a/Zend/tests/generics/reification/new_static_vs_self.phpt b/Zend/tests/generics/reification/new_static_vs_self.phpt new file mode 100644 index 000000000000..bbd8d32a0f8b --- /dev/null +++ b/Zend/tests/generics/reification/new_static_vs_self.phpt @@ -0,0 +1,45 @@ +--TEST-- +Reification: `new static()` reproduces the exact runtime type; `new self::<...>` re-applies the lexical template +--FILE-- +` remains the way to apply type arguments to the enclosing +// (lexical) class — including re-binding to different args. +class C { + public function __construct(private array $e = []) {} + + public function dupStatic(): static { return new static($this->e); } + public function dupSelf(): static { return new self::($this->e); } + public function swapSelf(): object { return new self::($this->e); } + public function reSelf(): object { return new self::($this->e); } +} + +$o = new C::([1, 2]); +echo "orig: ", $o::class, "\n"; +echo "dupStatic: ", $o->dupStatic()::class, "\n"; +echo "dupSelf: ", $o->dupSelf()::class, "\n"; +echo "swapSelf: ", $o->swapSelf()::class, "\n"; +echo "reSelf: ", $o->reSelf()::class, "\n"; + +// `new static()` through a non-generic subclass of a monomorph reproduces the +// subclass, exactly as plain LSB does. +class Box { + public function __construct(public mixed $v = null) {} + public function copy(): static { return new static($this->v); } +} +class IntBox extends Box {} + +$b = new IntBox(5); +echo "IntBox: ", $b::class, "\n"; +echo "IntBox copy: ", $b->copy()::class, "\n"; +?> +--EXPECT-- +orig: C +dupStatic: C +dupSelf: C +swapSelf: C +reSelf: C +IntBox: IntBox +IntBox copy: IntBox diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 4e0b39bd2c35..9688ec242fca 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8097,6 +8097,27 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ } } + /* `new static::<...>` is always rejected at compile time. The type + * arguments are written in the lexical scope — they name the enclosing + * class's (`self`'s) type parameters — but `static` is the *runtime* type, + * whose parameter list is unknown here and need not match: it may be a + * generic subclass with a different parameter list, or a non-generic + * subclass of a monomorph (e.g. `class IntBox extends Box`) with no + * type parameters at all. `static` already carries its own bindings, so + * `new static()` reproduces the exact runtime type; `new self::<...>` + * re-applies the lexical template. */ + if (opline->op1_type == IS_UNUSED && turbofish_ast + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC) { + zend_ast_list *tf_list = zend_ast_get_list(turbofish_ast); + if (tf_list->children > 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type arguments cannot be applied to \"static\": " + "\"static\" is the runtime type and already carries its type arguments. " + "Use \"new static()\" to construct the exact runtime type, " + "or \"new self::<...>\" to apply type arguments to the enclosing class"); + } + } + /* Reject turbofish on a known non-generic class at compile time so the * error matches the type-position behavior (rather than waiting for * VERIFY_GENERIC_ARGUMENTS to throw ArgumentCountError at runtime). */ From 1692bfdf59232c4cfca50c36057ef46b563d84a9 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Jun 2026 12:08:52 +0700 Subject: [PATCH 29/30] preload inference and compile time monomorphization without explicit turbofish --- Zend/Optimizer/dfa_pass.c | 600 ++++++++++++++++++ Zend/Optimizer/zend_optimizer.c | 4 +- Zend/Optimizer/zend_optimizer.h | 2 + Zend/Optimizer/zend_optimizer_internal.h | 2 + .../inference_all_defaulted_no_crash.phpt | 27 + .../reification/inference_literal_arg.phpt | 26 + .../reification/inference_optimizer_ssa.phpt | 45 ++ .../reification/preload_aot_call_lowering.inc | 28 + .../preload_aot_call_lowering.phpt | 36 ++ .../preload_class_monomorph_new.inc | 27 + .../preload_class_monomorph_new.phpt | 27 + .../reification/preload_class_typed_param.inc | 18 + .../preload_class_typed_param.phpt | 35 + .../reification/preload_generic_call_ops.inc | 10 + .../reification/preload_generic_call_ops.phpt | 25 + .../turbofish_namespace_class_arg.phpt | 36 ++ Zend/zend_compile.c | 191 +++++- Zend/zend_compile.h | 8 + Zend/zend_execute.c | 5 +- Zend/zend_inheritance.c | 266 +++++++- Zend/zend_inheritance.h | 6 + Zend/zend_opcode.c | 7 +- Zend/zend_vm_def.h | 19 +- Zend/zend_vm_execute.h | 38 +- ext/opcache/ZendAccelerator.c | 15 +- ext/opcache/zend_persist.c | 8 +- ext/opcache/zend_persist_calc.c | 6 + 27 files changed, 1479 insertions(+), 38 deletions(-) create mode 100644 Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt create mode 100644 Zend/tests/generics/reification/inference_literal_arg.phpt create mode 100644 Zend/tests/generics/reification/inference_optimizer_ssa.phpt create mode 100644 Zend/tests/generics/reification/preload_aot_call_lowering.inc create mode 100644 Zend/tests/generics/reification/preload_aot_call_lowering.phpt create mode 100644 Zend/tests/generics/reification/preload_class_monomorph_new.inc create mode 100644 Zend/tests/generics/reification/preload_class_monomorph_new.phpt create mode 100644 Zend/tests/generics/reification/preload_class_typed_param.inc create mode 100644 Zend/tests/generics/reification/preload_class_typed_param.phpt create mode 100644 Zend/tests/generics/reification/preload_generic_call_ops.inc create mode 100644 Zend/tests/generics/reification/preload_generic_call_ops.phpt create mode 100644 Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c index 76ca8e5145f0..fde6642af247 100644 --- a/Zend/Optimizer/dfa_pass.c +++ b/Zend/Optimizer/dfa_pass.c @@ -27,6 +27,7 @@ #include "zend_call_graph.h" #include "zend_inference.h" #include "zend_dump.h" +#include "zend_inheritance.h" #ifndef ZEND_DEBUG_DFA # define ZEND_DEBUG_DFA ZEND_DEBUG @@ -403,6 +404,601 @@ static bool variable_defined_or_used_in_range(zend_ssa *ssa, int var, int start, return false; } +/* SSA arg types reflect the call-site value, not the declared type, so inferring T from them is sound. */ +static bool zend_dfa_send_concrete_type( + const zend_op_array *op_array, const zend_ssa *ssa, + const zend_op *send, zend_type *out) +{ + /* By-ref / unusual sends: the callee could observe a reference; skip. */ + if (send->opcode == ZEND_SEND_REF + || send->opcode == ZEND_SEND_VAR_NO_REF + || send->opcode == ZEND_SEND_VAR_NO_REF_EX + || send->opcode == ZEND_SEND_USER) { + return false; + } + + if (send->op1_type == IS_CONST) { + const zval *zv = CT_CONSTANT_EX(op_array, send->op1.constant); + switch (Z_TYPE_P(zv)) { + case IS_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case IS_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case IS_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + default: return false; + } + } + + int var = ssa->ops[send - op_array->opcodes].op1_use; + if (var < 0) { + return false; + } + const zend_ssa_var_info *info = &ssa->var_info[var]; + if (info->type & (MAY_BE_UNDEF | MAY_BE_REF)) { + return false; + } + uint32_t pure = info->type & MAY_BE_ANY; + switch (pure) { + case MAY_BE_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case MAY_BE_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case MAY_BE_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + } + /* Exact class only: !is_instanceof matches what runtime inference binds T to. */ + if (pure == MAY_BE_OBJECT && info->ce && !info->is_instanceof) { + *out = (zend_type) ZEND_TYPE_INIT_CLASS(zend_string_copy(info->ce->name), 0, 0); + return true; + } + return false; +} + +static zend_op *zend_dfa_find_call_verify(zend_op_array *op_array, const zend_call_info *call_info) +{ + if (!call_info->caller_call_opline || !call_info->caller_init_opline) { + return NULL; + } + zend_op *p = call_info->caller_call_opline; + while (p > call_info->caller_init_opline) { + p--; + if (p->opcode == ZEND_NOP || p->opcode == ZEND_EXT_NOP + || p->opcode == ZEND_EXT_FCALL_BEGIN) { + continue; + } + /* VERIFY extended_value 0 = speculative non-turbofish site, != 0 = turbofish; INSTALL always turbofish. */ + if ((p->opcode == ZEND_VERIFY_GENERIC_ARGUMENTS + || p->opcode == ZEND_INSTALL_GENERIC_ARGS) + && p->op1_type == IS_UNUSED) { + return p; + } + return NULL; + } + return NULL; +} + +static bool zend_dfa_try_direct_dispatch(zend_op_array *op_array, + const zend_call_info *ci, zend_op *site, const zend_function *fbc) +{ + if (!op_array->generic_types || !op_array->generic_types->turbofish_args) { + return false; + } + zend_op *init = ci->caller_init_opline; + if (!init || (init->opcode != ZEND_INIT_FCALL + && init->opcode != ZEND_INIT_FCALL_BY_NAME + && init->opcode != ZEND_INIT_NS_FCALL_BY_NAME)) { + return false; + } + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, site->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + return false; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_type_parameter(nwa->args[i])) { + return false; + } + } + + zend_string *mangled = zend_generic_canonical_class_name( + fbc->common.function_name, nwa->args, nwa->count); + zend_string *lc = zend_string_tolower(mangled); + zend_string_hash_val(mangled); + zend_string_hash_val(lc); + zval zv; + ZVAL_STR(&zv, mangled); + uint32_t lit = zend_optimizer_add_literal(op_array, &zv); /* orig name */ + ZVAL_STR(&zv, lc); + zend_optimizer_add_literal(op_array, &zv); /* lc name at lit+1 */ + + init->opcode = ZEND_INIT_FCALL_BY_NAME; + init->op1_type = IS_UNUSED; + init->op1.num = 0; + init->op2_type = IS_CONST; + init->op2.constant = lit; + /* result.num keeps the existing 1-slot function cache. */ + MAKE_NOP(site); + return true; +} + +/* On a final class the called scope is always one of its own monomorphs, so static ≡ the own-params turbofish. */ +static uint32_t zend_dfa_selfize_generic_new(zend_op_array *op_array) +{ + zend_class_entry *scope = op_array->scope; + if (!scope || !scope->generic_parameters + || !(scope->ce_flags & ZEND_ACC_FINAL) + || !op_array->generic_types || !op_array->generic_types->turbofish_args) { + return 0; + } + uint32_t pcount = scope->generic_parameters->count; + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *verify = &op_array->opcodes[i]; + if (verify->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS + || verify->op1_type == IS_UNUSED + || verify->extended_value == 0) { + continue; + } + zend_op *newop = NULL; + for (uint32_t j = i; j > 0; j--) { + zend_op *p = &op_array->opcodes[j - 1]; + if (p->opcode == ZEND_NEW + && p->result_type == IS_TMP_VAR + && p->result.var == verify->op1.var) { + newop = p; + break; + } + } + if (!newop || newop->op1_type != IS_CONST) { + continue; + } + zend_string *cname = Z_STR(op_array->literals[newop->op1.constant]); + if (!zend_string_equals_ci(cname, scope->name)) { + continue; + } + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, verify->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + continue; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + if (nwa->count != pcount) { + continue; + } + bool identity = true; + for (uint32_t a = 0; a < nwa->count; a++) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(nwa->args[a]) + || (ZEND_TYPE_FULL_MASK(nwa->args[a]) & MAY_BE_NULL)) { + identity = false; + break; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(nwa->args[a]); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE || ref->index != a) { + identity = false; + break; + } + } + if (!identity) { + continue; + } + newop->op1_type = IS_UNUSED; + newop->op1.num = ZEND_FETCH_CLASS_STATIC | ZEND_FETCH_CLASS_EXCEPTION; + newop->op2_type = IS_UNUSED; + newop->op2.num = 0; + MAKE_NOP(verify); + changed++; + } + return changed; +} + +static uint32_t zend_dfa_optimize_generic_calls(zend_op_array *op_array, zend_ssa *ssa) +{ + const zend_func_info *func_info = ZEND_FUNC_INFO(op_array); + uint32_t changed = 0; + + if (!func_info || !func_info->callee_info) { + return 0; + } + + for (const zend_call_info *ci = func_info->callee_info; ci; ci = ci->next_callee) { + const zend_function *fbc = ci->callee_func; + if (!fbc || ci->is_prototype || ci->named_args || ci->send_unpack) { + continue; + } + if (!ZEND_USER_CODE(fbc->common.type)) { + continue; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params || params->count == 0) { + continue; + } + + zend_op *verify = zend_dfa_find_call_verify(op_array, ci); + if (!verify) { + continue; + } + + /* Turbofish site needs no value inference, so runs even for callees with no inferable params. */ + if (verify->extended_value != 0) { + if (zend_dfa_try_direct_dispatch(op_array, ci, verify, fbc)) { + changed++; + continue; + } + uint8_t opcode = zend_generic_try_install_resolved_turbofish( + op_array, fbc, verify->extended_value, verify->op2.num); + if (opcode != ZEND_NOP) { + verify->opcode = opcode; + changed++; + } + continue; + } + + if (params->inferable_mask == 0 + || !fbc->op_array.generic_types || !fbc->op_array.generic_types->parameters) { + continue; + } + + uint32_t total = params->count; + uint32_t required = 0; + while (required < total + && !ZEND_TYPE_IS_SET(params->parameters[required].default_type)) { + required++; + } + if (total > ZEND_GENERIC_MAX_PARAMS) { + continue; + } + + /* Bare top-level T only, matching zend_build_generic_call_type_args. */ + int arg_pos_for_gp[ZEND_GENERIC_MAX_PARAMS]; + for (uint32_t i = 0; i < total; i++) { + arg_pos_for_gp[i] = -1; + } + HashTable *pre = fbc->op_array.generic_types->parameters; + zend_ulong arg_idx; + zend_type *pe; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe)) continue; + if (ZEND_TYPE_FULL_MASK(*pe) & MAY_BE_NULL) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= total) continue; + if (arg_pos_for_gp[ref->index] == -1 && arg_idx < ci->num_args) { + arg_pos_for_gp[ref->index] = (int) arg_idx; + } + } ZEND_HASH_FOREACH_END(); + + zend_type inferred[ZEND_GENERIC_MAX_PARAMS]; + uint32_t arity = 0; + for (uint32_t g = 0; g < total; g++) { + int pos = arg_pos_for_gp[g]; + if (pos < 0 || !ci->arg_info[pos].opline) { + break; + } + zend_type entry; + if (!zend_dfa_send_concrete_type(op_array, ssa, ci->arg_info[pos].opline, &entry)) { + break; + } + inferred[arity++] = entry; + } + /* arity 0 would underflow ZEND_TYPE_NAMED_WITH_ARGS_SIZE; stay generic. */ + if (arity == 0 || arity < required) { + for (uint32_t i = 0; i < arity; i++) { + zend_type_release(inferred[i], /* persistent */ false); + } + continue; + } + + uint32_t args_id = 0; + uint8_t opcode = zend_generic_install_inferred_call( + op_array, fbc, inferred, arity, &args_id); + if (opcode == ZEND_NOP) { + continue; + } + verify->opcode = opcode; + verify->op2.num = arity; + verify->extended_value = args_id; + zend_dfa_try_direct_dispatch(op_array, ci, verify, fbc); + changed++; + } + + return changed; +} + +static uint32_t zend_aot_rewrite_new_sites(zend_op_array *op_array) +{ + if (!op_array->generic_types || !op_array->generic_types->turbofish_args + || !op_array->generic_types->monomorph_type_args) { + return 0; + } + const zend_type_arg_table *binds = op_array->generic_types->monomorph_type_args; + if (binds->count > ZEND_GENERIC_MAX_PARAMS) { + return 0; + } + zend_type bindv[ZEND_GENERIC_MAX_PARAMS]; + uint32_t bindc = binds->count; + for (uint32_t i = 0; i < bindc; i++) { + const zend_type *t = zend_type_arg_entry_type(&binds->entries[i]); + bindv[i] = t ? *t : (zend_type) ZEND_TYPE_INIT_NONE(0); + } + + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *verify = &op_array->opcodes[i]; + if (verify->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS + || verify->op1_type == IS_UNUSED + || verify->extended_value == 0) { + continue; + } + zend_op *newop = NULL; + for (uint32_t j = i; j > 0; j--) { + zend_op *p = &op_array->opcodes[j - 1]; + if (p->opcode == ZEND_NEW + && p->result_type == IS_TMP_VAR + && p->result.var == verify->op1.var) { + newop = p; + break; + } + } + if (!newop || newop->op1_type != IS_CONST) { + continue; /* self/static/dynamic base: leave the runtime VERIFY. */ + } + + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, verify->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + continue; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + if (nwa->count == 0 || nwa->count > ZEND_GENERIC_MAX_PARAMS) { + continue; + } + zend_type resolved[ZEND_GENERIC_MAX_PARAMS]; + bool ok = true; + for (uint32_t a = 0; a < nwa->count; a++) { + zend_type r = bindc + ? zend_substitute_function_type_param(nwa->args[a], bindv, bindc) + : nwa->args[a]; + if (zend_type_contains_type_parameter(r)) { + ok = false; + break; + } + resolved[a] = r; + } + if (!ok) { + continue; + } + + zend_string *base_name = Z_STR(op_array->literals[newop->op1.constant]); + zend_string *canonical = zend_generic_canonical_class_name( + base_name, resolved, nwa->count); + zend_string *lc = zend_string_tolower(canonical); + zend_string_hash_val(canonical); + zend_string_hash_val(lc); + + zval zv; + ZVAL_STR(&zv, canonical); + uint32_t lit = zend_optimizer_add_literal(op_array, &zv); /* full name */ + ZVAL_STR(&zv, lc); + zend_optimizer_add_literal(op_array, &zv); /* lc at lit+1 */ + + newop->op1.constant = lit; /* keep op2.num cache slot (caches mono ce) */ + MAKE_NOP(verify); + changed++; + } + return changed; +} + +/* Per-monomorph ONCE: re-running zend_optimize_script would corrupt SSA. */ +static void zend_aot_optimize_monomorph(zend_op_array *op_array, zend_script *script, zend_long opt_level) +{ + if (op_array->last_try_catch) { + /* dfa bails on try/catch; do the SSA-free class-`new` rewrite, leave the rest generic. */ + zend_revert_pass_two(op_array); + zend_aot_rewrite_new_sites(op_array); + zend_redo_pass_two(op_array); + return; + } + zend_optimizer_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.arena = zend_arena_create(64 * 1024); + ctx.script = script; + ctx.optimization_level = opt_level; + + zend_func_info *func_info = zend_arena_calloc(&ctx.arena, 1, sizeof(zend_func_info)); + ZEND_SET_FUNC_INFO(op_array, func_info); + + zend_revert_pass_two(op_array); + zend_aot_rewrite_new_sites(op_array); + zend_analyze_calls(&ctx.arena, script, 0, op_array, func_info); + func_info->call_map = zend_build_call_map(&ctx.arena, func_info, op_array); + if (zend_dfa_analyze_op_array(op_array, &ctx, &func_info->ssa) == SUCCESS) { + zend_dfa_optimize_op_array(op_array, &ctx, &func_info->ssa, func_info->call_map); + } + zend_redo_pass_two(op_array); + + ZEND_SET_FUNC_INFO(op_array, NULL); + zend_arena_destroy(ctx.arena); +} + +/* Returns the case-preserved (+0) name, not the lc one: arg_info needs the original case. */ +static zend_string *zend_aot_mangled_call_name(const zend_op_array *op_array, const zend_op *op) +{ + if (op->op2_type != IS_CONST) { + return NULL; + } + zend_string *display; + if (op->opcode == ZEND_INIT_FCALL_BY_NAME || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) { + display = Z_STR_P(RT_CONSTANT(op, op->op2)); + } else { + return NULL; + } + return memchr(ZSTR_VAL(display), '<', ZSTR_LEN(display)) ? display : NULL; +} + +/* Synthesizes each direct-dispatch callee; its own calls become the next round's work (fixpoint). */ +ZEND_API uint32_t zend_aot_monomorphize_script(zend_script *script, zend_long opt_level) +{ + /* Collect names first: synthesis rehashes function_table mid-iteration. */ + zend_string *wanted[4096]; + uint32_t want_count = 0; + zend_op_array *op_array; + /* wanted[] holds display names; table is keyed by lc, so lowercase only for the existence check. */ + #define ZEND_AOT_SCAN_OPS(oa) do { \ + zend_op_array *zoa = (oa); \ + if (zoa->type == ZEND_USER_FUNCTION && zoa->opcodes) { \ + for (uint32_t _i = 0; _i < zoa->last && want_count < (sizeof(wanted)/sizeof(wanted[0])); _i++) { \ + zend_string *_disp = zend_aot_mangled_call_name(zoa, &zoa->opcodes[_i]); \ + if (_disp) { \ + zend_string *_lc = zend_string_tolower(_disp); \ + if (!zend_hash_exists(&script->function_table, _lc)) { \ + wanted[want_count++] = _disp; \ + } \ + zend_string_release(_lc); \ + } \ + } \ + } \ + } while (0) + ZEND_HASH_MAP_FOREACH_PTR(&script->function_table, op_array) { + ZEND_AOT_SCAN_OPS(op_array); + } ZEND_HASH_FOREACH_END(); + zend_class_entry *ce; + ZEND_HASH_MAP_FOREACH_PTR(&script->class_table, ce) { + zend_function *m; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, m) { + ZEND_AOT_SCAN_OPS(&m->op_array); + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + #undef ZEND_AOT_SCAN_OPS + + zend_op_array *new_monos[4096]; + uint32_t new_count = 0; + for (uint32_t i = 0; i < want_count; i++) { + zend_string *lc = zend_string_tolower(wanted[i]); + bool exists = zend_hash_exists(&script->function_table, lc); + zend_string_release(lc); + if (exists) { + continue; /* synthesized as a duplicate request earlier this round. */ + } + zend_function *mono = zend_synthesize_specialized_monomorph_by_name( + &script->function_table, wanted[i]); + if (mono && new_count < (sizeof(new_monos)/sizeof(new_monos[0]))) { + new_monos[new_count++] = &mono->op_array; + } + } + + for (uint32_t i = 0; i < new_count; i++) { + zend_aot_optimize_monomorph(new_monos[i], script, opt_level); + } + return new_count; +} + +/* Upgrade INIT_FCALL_BY_NAME -> INIT_FCALL (+ DO_UCALL): safe since AOT persists the callee. */ +static zend_always_inline bool zend_aot_is_call_open(uint8_t opcode) +{ + switch (opcode) { + case ZEND_INIT_FCALL: + case ZEND_INIT_FCALL_BY_NAME: + case ZEND_INIT_NS_FCALL_BY_NAME: + case ZEND_INIT_METHOD_CALL: + case ZEND_INIT_STATIC_METHOD_CALL: + case ZEND_INIT_DYNAMIC_CALL: + case ZEND_INIT_USER_CALL: + case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: + case ZEND_NEW: + return true; + default: + return false; + } +} + +static uint32_t zend_aot_upgrade_op_array_to_ucall(zend_op_array *op_array, zend_script *script) +{ + if (!op_array->opcodes) { + return 0; + } + /* Nesting stack matches each call-open to its closing DO_FCALL*; fbc resolved at INIT for the SENDs. */ + struct { uint32_t init_i; zend_function *fbc; } stack[512]; + uint32_t sp = 0; + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *op = &op_array->opcodes[i]; + if (zend_aot_is_call_open(op->opcode)) { + if (sp >= sizeof(stack)/sizeof(stack[0])) { + return changed; /* pathological nesting — stop, stay correct. */ + } + /* Both by-name forms keep the registered lc name at the +1 literal. */ + zend_function *fbc = NULL; + if ((op->opcode == ZEND_INIT_FCALL_BY_NAME + || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) + && op->op2_type == IS_CONST) { + zend_string *lc = Z_STR_P(RT_CONSTANT(op, op->op2) + 1); + if (memchr(ZSTR_VAL(lc), '<', ZSTR_LEN(lc))) { + zend_function *f = zend_hash_find_ptr(&script->function_table, lc); + if (f && f->type == ZEND_USER_FUNCTION) { + fbc = f; + } + } + } + stack[sp].init_i = i; + stack[sp].fbc = fbc; + sp++; + continue; + } + if (op->opcode == ZEND_CALLABLE_CONVERT) { + if (sp > 0) sp--; /* first-class callable closes its frame, no DO. */ + continue; + } + if (sp > 0 && stack[sp - 1].fbc + && op->opcode == ZEND_SEND_VAR_EX + && op->op2_type != IS_CONST + && !ARG_SHOULD_BE_SENT_BY_REF(stack[sp - 1].fbc, op->op2.num)) { + op->opcode = ZEND_SEND_VAR; + zend_vm_set_opcode_handler(op); + } + bool is_do = op->opcode == ZEND_DO_FCALL || op->opcode == ZEND_DO_FCALL_BY_NAME + || op->opcode == ZEND_DO_ICALL || op->opcode == ZEND_DO_UCALL; + if (!is_do || sp == 0) { + continue; + } + zend_function *fbc = stack[sp - 1].fbc; + zend_op *init = &op_array->opcodes[stack[sp - 1].init_i]; + sp--; + if (!fbc) { + continue; /* not a resolved monomorph dispatch site. */ + } + init->opcode = ZEND_INIT_FCALL; + init->op1_type = IS_UNUSED; + init->op1.num = zend_vm_calc_used_stack(init->extended_value, fbc); + /* INIT_FCALL reads op2 directly: advance one zval to the +1 lc literal it resolves. */ + init->op2.constant += sizeof(zval); + zend_vm_set_opcode_handler(init); + + /* Skip deprecated/nodiscard (as zend_get_call_op does) so warnings still fire. */ + if (op->opcode == ZEND_DO_FCALL_BY_NAME + && !(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED | ZEND_ACC_NODISCARD))) { + op->opcode = ZEND_DO_UCALL; + zend_vm_set_opcode_handler(op); + } + changed++; + } + return changed; +} + +ZEND_API uint32_t zend_aot_upgrade_dispatch_to_ucall(zend_script *script) +{ + uint32_t changed = 0; + zend_op_array *op_array; + ZEND_HASH_MAP_FOREACH_PTR(&script->function_table, op_array) { + changed += zend_aot_upgrade_op_array_to_ucall(op_array, script); + } ZEND_HASH_FOREACH_END(); + zend_class_entry *ce; + ZEND_HASH_MAP_FOREACH_PTR(&script->class_table, ce) { + zend_function *m; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, m) { + if (m->type == ZEND_USER_FUNCTION) { + changed += zend_aot_upgrade_op_array_to_ucall(&m->op_array, script); + } + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + return changed; +} + static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa) { const zend_func_info *func_info = ZEND_FUNC_INFO(op_array); @@ -1070,6 +1666,10 @@ void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx if (zend_dfa_optimize_calls(op_array, ssa)) { remove_nops = 1; } + zend_dfa_optimize_generic_calls(op_array, ssa); + if (zend_dfa_selfize_generic_new(op_array)) { + remove_nops = 1; + } } if (ctx->debug_level & ZEND_DUMP_AFTER_PASS_8) { zend_dump_op_array(op_array, ZEND_DUMP_SSA, "after sccp pass", ssa); diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index 5ffdefac49a3..26d6f1f425bb 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -1306,7 +1306,7 @@ static void zend_optimize(zend_op_array *op_array, } } -static void zend_revert_pass_two(zend_op_array *op_array) +void zend_revert_pass_two(zend_op_array *op_array) { zend_op *opline; @@ -1336,7 +1336,7 @@ static void zend_revert_pass_two(zend_op_array *op_array) op_array->fn_flags &= ~ZEND_ACC_DONE_PASS_TWO; } -static void zend_redo_pass_two(zend_op_array *op_array) +void zend_redo_pass_two(zend_op_array *op_array) { zend_op *opline, *end; #if ZEND_USE_ABS_JMP_ADDR && !ZEND_USE_ABS_CONST_ADDR diff --git a/Zend/Optimizer/zend_optimizer.h b/Zend/Optimizer/zend_optimizer.h index d2847c92869c..6d3ac2423e5d 100644 --- a/Zend/Optimizer/zend_optimizer.h +++ b/Zend/Optimizer/zend_optimizer.h @@ -92,6 +92,8 @@ typedef void (*zend_optimizer_pass_t)(zend_script *, void *context); BEGIN_EXTERN_C() ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_level, zend_long debug_level); +ZEND_API uint32_t zend_aot_monomorphize_script(zend_script *script, zend_long opt_level); +ZEND_API uint32_t zend_aot_upgrade_dispatch_to_ucall(zend_script *script); ZEND_API int zend_optimizer_register_pass(zend_optimizer_pass_t pass); ZEND_API void zend_optimizer_unregister_pass(int idx); zend_result zend_optimizer_startup(void); diff --git a/Zend/Optimizer/zend_optimizer_internal.h b/Zend/Optimizer/zend_optimizer_internal.h index d01df56260bc..9592183c503f 100644 --- a/Zend/Optimizer/zend_optimizer_internal.h +++ b/Zend/Optimizer/zend_optimizer_internal.h @@ -111,6 +111,8 @@ void zend_optimizer_pass3(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_cfg(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_dfa(zend_op_array *op_array, zend_optimizer_ctx *ctx); +void zend_revert_pass_two(zend_op_array *op_array); +void zend_redo_pass_two(zend_op_array *op_array); zend_result zend_dfa_analyze_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa); void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa, zend_call_info **call_map); void zend_optimize_temporary_variables(zend_op_array *op_array, zend_optimizer_ctx *ctx); diff --git a/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt b/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt new file mode 100644 index 000000000000..f72df5890e73 --- /dev/null +++ b/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: a non-turbofish call to an all-defaulted generic callee with non-literal args infers nothing and stays generic (must not build an empty type-arg box) +--FILE-- + crash). +function neighbors(mixed $graph, TNode $node): array { + return [$node]; +} + +function caller(mixed $g, mixed $n): array { + return neighbors($g, $n); +} + +var_dump(caller('graph', 'x')); +var_dump(caller('graph', 42)); +echo "ok\n"; +?> +--EXPECT-- +array(1) { + [0]=> + string(1) "x" +} +array(1) { + [0]=> + int(42) +} +ok diff --git a/Zend/tests/generics/reification/inference_literal_arg.phpt b/Zend/tests/generics/reification/inference_literal_arg.phpt new file mode 100644 index 000000000000..14829462fb20 --- /dev/null +++ b/Zend/tests/generics/reification/inference_literal_arg.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: a scalar literal argument monomorphizes a non-turbofish generic call at compile time (no opcache needed); the reified return type enforces the inferred T +--FILE-- +(T $x): T { return "s"; } +function retInt(T $x): T { return 7; } + +function lit_int() { return retStr(42); } +function lit_float() { return retStr(3.14); } +function lit_string() { return retInt("hi"); } + +foreach (['lit_int', 'lit_float', 'lit_string'] as $fn) { + try { + $fn(); + echo "$fn: NO error (not monomorphized!)\n"; + } catch (TypeError $e) { + echo "$fn: ", $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +lit_int: retStr(): Return value must be of type int, string returned +lit_float: retStr(): Return value must be of type float, string returned +lit_string: retInt(): Return value must be of type string, int returned diff --git a/Zend/tests/generics/reification/inference_optimizer_ssa.phpt b/Zend/tests/generics/reification/inference_optimizer_ssa.phpt new file mode 100644 index 000000000000..716957ccd5bf --- /dev/null +++ b/Zend/tests/generics/reification/inference_optimizer_ssa.phpt @@ -0,0 +1,45 @@ +--TEST-- +Reification: the opcache DFA optimizer monomorphizes non-turbofish generic calls from each argument's SSA-inferred type; reassignment binds the actual type, not the declared one, and unknown types stay generic +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +--FILE-- +(T $x): T { return "s"; } +function retInt(T $x): T { return 7; } + +function from_int(int $i) { return retStr($i); } +function from_float(float $f) { return retStr($f); } +function local_literal() { $v = 1.5; return retStr($v); } + +// Reassignment must bind T from the SSA type (string), not the declared int. +function reassigned(int $val) { $val = 'x'; return retInt($val); } + +function from_unknown(array $a) { $v = $a[0]; return retInt($v); } + +function check(string $name, callable $fn): void { + try { + $r = $fn(); + echo "$name: ", var_export($r, true), "\n"; + } catch (TypeError $e) { + echo "$name: TypeError\n"; + } +} + +check('from_int', fn() => from_int(1)); +check('from_float', fn() => from_float(1.5)); +check('local_literal', fn() => local_literal()); +check('reassigned', fn() => reassigned(1)); +check('from_unknown', fn() => from_unknown([42])); +?> +--EXPECT-- +from_int: TypeError +from_float: TypeError +local_literal: TypeError +reassigned: TypeError +from_unknown: 7 diff --git a/Zend/tests/generics/reification/preload_aot_call_lowering.inc b/Zend/tests/generics/reification/preload_aot_call_lowering.inc new file mode 100644 index 000000000000..b86db5d7f720 --- /dev/null +++ b/Zend/tests/generics/reification/preload_aot_call_lowering.inc @@ -0,0 +1,28 @@ +(T $x): T { return $x; } + +function add(T $a, T $b): T { return $a + $b; } + +// No value params: dispatch must fire from the turbofish alone, nothing inferable from args. +function zero(): string { return "z"; } + +class Runner { + public static function ints(int $n): int { + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = add::($acc, id::($i)); + } + return $acc; + } + + public static function strs(): string { + return id::("hello") . ":" . zero::(); + } + + public static function badType(): void { + add::(1, []); + } +} diff --git a/Zend/tests/generics/reification/preload_aot_call_lowering.phpt b/Zend/tests/generics/reification/preload_aot_call_lowering.phpt new file mode 100644 index 000000000000..f35cd4e034af --- /dev/null +++ b/Zend/tests/generics/reification/preload_aot_call_lowering.phpt @@ -0,0 +1,36 @@ +--TEST-- +Reification: AOT-lowered preloaded generic calls (INIT_FCALL/DO_UCALL + concrete RECV + by-value SEND) stay correct and type-safe +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_aot_call_lowering.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + 5. +var_dump(Bench\add::("5", 2)); + +try { + Runner::badType(); +} catch (\TypeError $e) { + echo $e->getMessage(), "\n"; +} +echo "done\n"; +?> +--EXPECTF-- +int(10) +string(7) "hello:z" +int(7) +Bench\add(): Argument #2 ($b) must be of type int, array given, called in %s on line %d +done diff --git a/Zend/tests/generics/reification/preload_class_monomorph_new.inc b/Zend/tests/generics/reification/preload_class_monomorph_new.inc new file mode 100644 index 000000000000..15ca454caaed --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_monomorph_new.inc @@ -0,0 +1,27 @@ + { + public function __construct(public array $items = []) {} + public function with(T $x): Vec { + $n = $this->items; + $n[] = $x; + return new Vec::($n); + } + public function sum(): int { + $s = 0; + foreach ($this->items as $v) { $s += $v; } + return $s; + } + public function size(): int { return count($this->items); } +} + +class Driver { + public static function build(): string { + $v = new Vec::([1, 2]); + $v = $v->with(3); + $v = $v->with(4); + return $v->sum() . ':' . $v->size() . ':' . get_class($v); + } +} diff --git a/Zend/tests/generics/reification/preload_class_monomorph_new.phpt b/Zend/tests/generics/reification/preload_class_monomorph_new.phpt new file mode 100644 index 000000000000..44c9911d2cf2 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_monomorph_new.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: a compile-time class monomorph (`new C::`) inside a preloaded method preloads cleanly and behaves correctly +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_class_monomorph_new.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + SEGV at preload. +echo Driver::build(), "\n"; + +$s = new Vec::(['a']); +$s = $s->with('b'); +echo $s->size(), ':', get_class($s), "\n"; +echo "done\n"; +?> +--EXPECT-- +10:4:Vec +2:Vec +done diff --git a/Zend/tests/generics/reification/preload_class_typed_param.inc b/Zend/tests/generics/reification/preload_class_typed_param.inc new file mode 100644 index 000000000000..d48ba9710738 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_typed_param.inc @@ -0,0 +1,18 @@ +(T $x): T { return $x; } + +class Keeper { + public static function take(Animal $a): string { + return get_class(keep::($a)); + } + public static function bad(): void { + keep::("not an animal"); + } +} diff --git a/Zend/tests/generics/reification/preload_class_typed_param.phpt b/Zend/tests/generics/reification/preload_class_typed_param.phpt new file mode 100644 index 000000000000..cb1fc27f1505 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_typed_param.phpt @@ -0,0 +1,35 @@ +--TEST-- +Reification: an AOT-preloaded monomorph of a bare class-typed parameter accepts subclasses and rejects non-instances +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_class_typed_param.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; +} +echo "done\n"; +?> +--EXPECTF-- +Zoo\Dog +Zoo\Animal +Zoo\keep(): Argument #1 ($x) must be of type Zoo\Animal, string given, called in %s on line %d +done diff --git a/Zend/tests/generics/reification/preload_generic_call_ops.inc b/Zend/tests/generics/reification/preload_generic_call_ops.inc new file mode 100644 index 000000000000..b02304b58327 --- /dev/null +++ b/Zend/tests/generics/reification/preload_generic_call_ops.inc @@ -0,0 +1,10 @@ +(T $x): T { + return $x; +} + +function outer(T $x): T { + return inner($x); +} diff --git a/Zend/tests/generics/reification/preload_generic_call_ops.phpt b/Zend/tests/generics/reification/preload_generic_call_ops.phpt new file mode 100644 index 000000000000..871747b8e8d6 --- /dev/null +++ b/Zend/tests/generics/reification/preload_generic_call_ops.phpt @@ -0,0 +1,25 @@ +--TEST-- +Reification: a preloaded generic function that contains a generic call op resolves correctly and shuts down cleanly (smoke test for the destroy_op_array runtime-cache cleanup; the underlying UAF is caught deterministically under valgrind/ASAN) +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_generic_call_ops.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +(5)); +var_dump(outer(7)); +var_dump(inner::("ok")); +echo "done\n"; +?> +--EXPECT-- +int(5) +int(7) +string(2) "ok" +done diff --git a/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt b/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt new file mode 100644 index 000000000000..790d0bf63333 --- /dev/null +++ b/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt @@ -0,0 +1,36 @@ +--TEST-- +Reification: an unqualified class name in a turbofish type argument resolves against the current namespace +--FILE-- +(T $x): T { return $x; } + +$dog = identity::(new Dog()); +echo get_class($dog), "\n"; + +$animal = identity::(new Animal()); +echo get_class($animal), "\n"; + +try { + identity::("not an animal"); +} catch (\TypeError $e) { + echo $e->getMessage(), "\n"; +} + +function wrap(T $x): T { return $x; } +$d2 = wrap::(new Dog()); +echo get_class($d2), "\n"; + +echo "done\n"; +?> +--EXPECTF-- +Acme\Dog +Acme\Animal +Acme\identity(): Argument #1 ($x) must be of type Acme\Animal, string given, called in %s on line %d +Acme\Dog +done diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 9688ec242fca..2adea3d829be 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -438,6 +438,9 @@ void zend_init_compiler_data_structures(void) /* {{{ */ CG(generic_scope) = NULL; CG(in_static_member_type) = false; CG(inheritance_binding_cache) = NULL; + CG(inheritance_binding_hint).target = NULL; + CG(inheritance_binding_hint).args = NULL; + CG(inheritance_binding_hint).arity = 0; } /* }}} */ @@ -1757,7 +1760,7 @@ static zend_type_arg_table *zend_build_concrete_call_type_args( /* If this turbofish CALL site is concrete, build the resolved type-arg table now * and stash it on the turbofish entry so persist can relocate it into SHM. */ -static bool zend_try_attach_concrete_call_table( +static bool zend_try_attach_concrete_call_table_for(zend_op_array *caller, const zend_function *fbc, const zend_type *args_box, uint32_t args_id) { if (!fbc || !args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { @@ -1776,7 +1779,7 @@ static bool zend_try_attach_concrete_call_table( if (!ct) { return false; } - zend_generic_type_table *gtt = CG(active_op_array)->generic_types; + zend_generic_type_table *gtt = caller->generic_types; zend_turbofish_args_entry *entry = zend_hash_index_find_ptr(gtt->turbofish_args, args_id); ZEND_ASSERT(entry != NULL); @@ -1789,7 +1792,169 @@ static bool zend_try_attach_concrete_call_table( return true; } -static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc) +/* Literals only: inferring T from a variable's declared type is unsound, as the + * variable may be reassigned before the call. */ +static bool zend_infer_literal_arg_pre_erasure(zend_ast *arg, zend_type *out) +{ + if (arg->kind != ZEND_AST_ZVAL) { + return false; + } + const zval *zv = zend_ast_get_zval(arg); + switch (Z_TYPE_P(zv)) { + case IS_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case IS_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case IS_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + default: return false; + } +} + +static const zend_type *zend_infer_call_type_args( + const zend_function *fbc, zend_ast *args_ast, + uint32_t *out_arity, uint32_t *out_args_id) +{ + if (!fbc || !ZEND_USER_CODE(fbc->common.type)) { + return NULL; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params || params->count == 0 || params->inferable_mask == 0) { + return NULL; + } + const zend_op_array *callee = &fbc->op_array; + if (!callee->generic_types || !callee->generic_types->parameters) { + return NULL; + } + if (!args_ast || args_ast->kind != ZEND_AST_ARG_LIST) { + return NULL; + } + zend_ast_list *args = zend_ast_get_list(args_ast); + for (uint32_t i = 0; i < args->children; i++) { + zend_ast_kind k = args->child[i]->kind; + if (k == ZEND_AST_UNPACK || k == ZEND_AST_NAMED_ARG) { + return NULL; + } + } + + uint32_t required, total; + zend_compute_generic_required_total(params, &required, &total); + if (total == 0 || total > ZEND_GENERIC_MAX_PARAMS) { + return NULL; + } + + int arg_pos_for_gp[ZEND_GENERIC_MAX_PARAMS]; + for (uint32_t i = 0; i < total; i++) { + arg_pos_for_gp[i] = -1; + } + { + HashTable *pre = callee->generic_types->parameters; + zend_ulong arg_idx; + zend_type *pe_type_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + if (ZEND_TYPE_FULL_MASK(*pe_type_ptr) & MAY_BE_NULL) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= total) continue; + if (arg_pos_for_gp[ref->index] == -1 && arg_idx < (zend_ulong) args->children) { + arg_pos_for_gp[ref->index] = (int) arg_idx; + } + } ZEND_HASH_FOREACH_END(); + } + + zend_type inferred[ZEND_GENERIC_MAX_PARAMS]; + uint32_t arity = 0; + for (uint32_t g = 0; g < total; g++) { + int pos = arg_pos_for_gp[g]; + if (pos < 0) { + break; + } + zend_type entry; + if (!zend_infer_literal_arg_pre_erasure(args->child[pos], &entry)) { + break; + } + inferred[arity++] = entry; + } + + if (arity == 0 || arity < required) { + for (uint32_t i = 0; i < arity; i++) { + zend_type_release(inferred[i], /* persistent */ false); + } + return NULL; + } + + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(arity)); + payload->name = NULL; + payload->name_attr = 0; + payload->count = arity; + for (uint32_t i = 0; i < arity; i++) { + payload->args[i] = inferred[i]; + } + zend_type box = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(box, payload); + ZEND_TYPE_FULL_MASK(box) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + uint32_t args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, box); + + *out_arity = arity; + *out_args_id = args_id; + return zend_hash_index_find_ptr(table->turbofish_args, args_id); +} + +ZEND_API uint8_t zend_generic_install_inferred_call(zend_op_array *caller, + const zend_function *fbc, zend_type *arg_types, uint32_t arity, + uint32_t *out_args_id) +{ + /* arity == 0 would underflow ZEND_TYPE_NAMED_WITH_ARGS_SIZE (count-1). */ + if (arity == 0) { + return ZEND_NOP; + } + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(arity)); + payload->name = NULL; + payload->name_attr = 0; + payload->count = arity; + for (uint32_t i = 0; i < arity; i++) { + payload->args[i] = arg_types[i]; + } + zend_type box = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(box, payload); + ZEND_TYPE_FULL_MASK(box) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + + if (!zend_can_install_call_args_statically(fbc, &box, arity)) { + zend_type_release(box, /* persistent */ false); + return ZEND_NOP; + } + + zend_generic_type_table *table = zend_generic_get_or_create_op_array_table(caller); + uint32_t args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, box); + const zend_type *args_box = zend_hash_index_find_ptr(table->turbofish_args, args_id); + zend_try_attach_concrete_call_table_for(caller, fbc, args_box, args_id); + + *out_args_id = args_id; + return ZEND_INSTALL_GENERIC_ARGS; +} + +ZEND_API uint8_t zend_generic_try_install_resolved_turbofish(zend_op_array *caller, + const zend_function *fbc, uint32_t args_id, uint32_t arity) +{ + if (!caller->generic_types || !caller->generic_types->turbofish_args) { + return ZEND_NOP; + } + const zend_turbofish_args_entry *entry = + zend_hash_index_find_ptr(caller->generic_types->turbofish_args, args_id); + if (!entry) { + return ZEND_NOP; + } + if (!zend_can_install_call_args_statically(fbc, &entry->args_box, arity)) { + return ZEND_NOP; + } + zend_try_attach_concrete_call_table_for(caller, fbc, &entry->args_box, args_id); + return ZEND_INSTALL_GENERIC_ARGS; +} + +static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc, zend_ast *call_args_ast) { uint32_t arity = 0; uint32_t args_id = 0; @@ -1817,6 +1982,13 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t || !fbc->op_array.generic_parameters)) { return; } + if (fbc) { + const zend_type *inferred = + zend_infer_call_type_args(fbc, call_args_ast, &arity, &args_id); + if (inferred) { + args_box = inferred; + } + } } /* Replace the runtime VERIFY with INSTALL when arity+bound check is @@ -1826,11 +1998,11 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t if (kind == ZEND_VERIFY_ARITY_KIND_CALL && zend_can_install_call_args_statically(fbc, args_box, arity)) { opcode = ZEND_INSTALL_GENERIC_ARGS; - zend_try_attach_concrete_call_table(fbc, args_box, args_id); - } else if (kind == ZEND_VERIFY_ARITY_KIND_CALL && turbofish_ast && args_id) { + zend_try_attach_concrete_call_table_for(CG(active_op_array), fbc, args_box, args_id); + } else if (kind == ZEND_VERIFY_ARITY_KIND_CALL && args_id) { /* Concrete args but VERIFY stays (bounds not statically decidable); * still attach the invariant table for the handler to install. */ - zend_try_attach_concrete_call_table(fbc, args_box, args_id); + zend_try_attach_concrete_call_table_for(CG(active_op_array), fbc, args_box, args_id); } zend_op *opline = get_next_op(); @@ -2201,7 +2373,8 @@ static zend_type zend_compile_pre_erasure_typename(zend_ast *ast) result = (zend_type) ZEND_TYPE_INIT_CODE(code, 0, 0); } } else { - zend_string_addref(name); + /* Resolve against namespace/use, else `f::` in `namespace N` stays "C" not "N\C". */ + name = zend_resolve_class_name(name, ast->attr); result = (zend_type) ZEND_TYPE_INIT_CLASS(name, 0, 0); } } @@ -6242,7 +6415,7 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze * monomorph's substituted arg_info; CALL emits it after args so inference * from arg values can fill type-parameter slots. */ if (verify_kind == ZEND_VERIFY_ARITY_KIND_NEW) { - zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc); + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc, NULL); } if (args_ast->kind == ZEND_AST_CALLABLE_CONVERT) { @@ -6279,7 +6452,7 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze uint32_t arg_count = zend_compile_args(args_ast, fbc, &may_have_extra_named_args); if (verify_kind == ZEND_VERIFY_ARITY_KIND_CALL) { - zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc); + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc, args_ast); } zend_do_extended_fcall_begin(); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index cf748fa4ce69..3781d3cb740e 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -211,6 +211,14 @@ ZEND_API void zend_generic_type_table_set_implements(zend_generic_type_table *t, ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, uint32_t idx, zend_type type); ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type); +/* Takes ownership of arg_types. */ +ZEND_API uint8_t zend_generic_install_inferred_call(zend_op_array *caller, + const zend_function *fbc, zend_type *arg_types, uint32_t arity, + uint32_t *out_args_id); + +ZEND_API uint8_t zend_generic_try_install_resolved_turbofish(zend_op_array *caller, + const zend_function *fbc, uint32_t args_id, uint32_t arity); + ZEND_API void zend_check_generic_param_list_size(zend_ast *list_ast); ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast); diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 2502878ff8ea..2ea5c6d349f6 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4303,7 +4303,10 @@ static zend_always_inline void i_free_compiled_variables(zend_execute_data *exec count--; } if (UNEXPECTED(execute_data->type_args)) { - zend_type_arg_table_destroy(execute_data->type_args); + /* Persisted monomorph tables are not frame-owned; only free request-built tables. */ + if (!execute_data->type_args->persisted) { + zend_type_arg_table_destroy(execute_data->type_args); + } execute_data->type_args = NULL; } } diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index a961d8b4a2e9..cb68f34cf1d1 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -7771,7 +7771,7 @@ ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( /* Build a concrete arg_info block by substituting the FUNCTION_LIKE T-refs in * the base's pre-erasure generic types with the concrete args. */ static zend_arg_info *zend_monomorph_build_arg_info( - const zend_op_array *base, const zend_type *args, uint32_t arity) + const zend_op_array *base, const zend_type *args, uint32_t arity, bool use_arena) { uint32_t num_args = base->num_args; bool has_return = (base->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; @@ -7782,7 +7782,9 @@ static zend_arg_info *zend_monomorph_build_arg_info( } const zend_arg_info *orig_block = base->arg_info - (has_return ? 1 : 0); - zend_arg_info *new_block = zend_arena_alloc(&CG(arena), sizeof(zend_arg_info) * total); + zend_arg_info *new_block = use_arena + ? zend_arena_alloc(&CG(arena), sizeof(zend_arg_info) * total) + : emalloc(sizeof(zend_arg_info) * total); memcpy(new_block, orig_block, sizeof(zend_arg_info) * total); const HashTable *pre_params = base->generic_types ? base->generic_types->parameters : NULL; @@ -7823,10 +7825,11 @@ static zend_arg_info *zend_monomorph_build_arg_info( } } if (substitute) { - zend_type_copy_ctor(&sub, /* use_arena */ true, /* persistent */ false); + /* use_arena false on the AOT/preload path: emalloc, not arena. */ + zend_type_copy_ctor(&sub, use_arena, /* persistent */ false); new_block[slot].type = sub; } else { - zend_type_copy_ctor(&new_block[slot].type, /* use_arena */ true, /* persistent */ false); + zend_type_copy_ctor(&new_block[slot].type, use_arena, /* persistent */ false); } if (new_block[slot].name) { zend_string_addref(new_block[slot].name); @@ -7902,12 +7905,16 @@ ZEND_API zend_function *zend_synthesize_function_monomorph( zend_function *existing = zend_hash_find_ptr(EG(function_table), lc_name); if (existing) { + /* Preload reaches INSTALL via here, not zend_fetch_function: init run_time_cache or cached opcodes hit NULL. */ + if (existing->type == ZEND_USER_FUNCTION) { + zend_init_func_run_time_cache(&existing->op_array); + } zend_string_release(display_name); zend_string_release(lc_name); return existing; } - zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(&base->op_array, args, arity); + zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(&base->op_array, args, arity, true); zend_op_array *mono = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); memcpy(mono, &base->op_array, sizeof(zend_op_array)); @@ -7983,6 +7990,214 @@ ZEND_API zend_function *zend_synthesize_function_monomorph( return mono_fn; } +ZEND_API zend_function *zend_synthesize_specialized_monomorph_into( + HashTable *fn_table, zend_function *base, const zend_type *args, uint32_t arity) +{ +#if ZEND_USE_ABS_CONST_ADDR + /* The combined opcode+literal buffer below assumes relative literal layout; caller uses shared synth. */ + (void) fn_table; (void) base; (void) args; (void) arity; + return NULL; +#else + if (!fn_table) { + fn_table = EG(function_table); + } + if (!base || base->type != ZEND_USER_FUNCTION) { + return NULL; + } + const zend_generic_parameter_list *params = base->op_array.generic_parameters; + if (!params || params->count == 0) { + return NULL; + } + + zend_type filled[ZEND_GENERIC_MAX_PARAMS]; + uint32_t total = params->count; + if (arity > total) { + return NULL; + } + if (arity < total) { + for (uint32_t i = 0; i < arity; i++) filled[i] = args[i]; + for (uint32_t i = arity; i < total; i++) { + const zend_generic_parameter *p = ¶ms->parameters[i]; + const zend_type *def = ZEND_TYPE_IS_SET(p->default_pre_erasure) + ? &p->default_pre_erasure + : (ZEND_TYPE_IS_SET(p->default_type) ? &p->default_type : NULL); + if (!def) { + return NULL; + } + filled[i] = *def; + } + args = filled; + arity = total; + } + for (uint32_t i = 0; i < arity; i++) { + if (zend_type_contains_type_parameter(args[i])) { + return NULL; + } + } + + zend_string *display_name = zend_generic_canonical_class_name( + base->common.function_name, args, arity); + zend_string *lc_name = zend_string_tolower(display_name); + + zend_function *existing = zend_hash_find_ptr(fn_table, lc_name); + if (existing) { + zend_string_release(display_name); + zend_string_release(lc_name); + return existing; + } + + /* Unlike the shared synth, opcodes+literals are a fresh OWN copy the optimizer rewrites in place. */ + const zend_op_array *src = &base->op_array; + zend_op_array *mono = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(mono, src, sizeof(zend_op_array)); + /* reserved[] memcpy'd from base may point into a transient arena: clear to avoid a dangling pointer. */ + memset(mono->reserved, 0, sizeof(mono->reserved)); + + /* persist efree()s opcodes and _put_free()s arg_info/vars/etc., so each must be an OWN heap alloc. */ + size_t ops_sz = ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_op) * src->last, 16); + size_t lit_sz = sizeof(zval) * src->last_literal; + char *buf = emalloc(ops_sz + lit_sz); + memcpy(buf, src->opcodes, ops_sz); + mono->opcodes = (zend_op *) buf; + if (src->last_literal) { + zval *dst_lit = (zval *) (buf + ops_sz); + memcpy(dst_lit, src->literals, lit_sz); + for (uint32_t i = 0; i < src->last_literal; i++) { + Z_TRY_ADDREF(dst_lit[i]); + } + mono->literals = dst_lit; + } + + if (src->last_var) { + mono->vars = emalloc(sizeof(zend_string *) * src->last_var); + for (uint32_t i = 0; i < src->last_var; i++) { + mono->vars[i] = zend_string_copy(src->vars[i]); + } + } else { + mono->vars = NULL; + } + + zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(src, args, arity, /* use_arena */ false); + + zend_type_arg_table *mono_targs = emalloc(ZEND_TYPE_ARG_TABLE_SIZE(total)); + mono_targs->count = total; + mono_targs->generation = 0; + mono_targs->persisted = false; + for (uint32_t i = 0; i < total; i++) { + mono_targs->entries[i].name = NULL; + mono_targs->entries[i].type_ref = NULL; + mono_targs->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); + if (i < arity && ZEND_TYPE_IS_SET(args[i])) { + zend_type owned = args[i]; + zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); + mono_targs->entries[i].owned_type = owned; + mono_targs->entries[i].name = zend_type_arg_canonical_name(args[i]); + } + } + { + zend_generic_type_table *gt = emalloc(sizeof(zend_generic_type_table)); + memset(gt, 0, sizeof(*gt)); + /* Deep-copy base's turbofish_args: the copied body references its boxes and persist frees the table (else double-free). */ + if (src->generic_types && src->generic_types->turbofish_args) { + zend_ulong tf_idx; + zend_turbofish_args_entry *tf_entry; + ZEND_HASH_FOREACH_NUM_KEY_PTR(src->generic_types->turbofish_args, tf_idx, tf_entry) { + zend_type box = tf_entry->args_box; + zend_type_copy_ctor(&box, /* use_arena */ false, /* persistent */ false); + zend_generic_type_table_set_turbofish_args(gt, (uint32_t) tf_idx, box); + } ZEND_HASH_FOREACH_END(); + } + gt->persisted = false; + gt->monomorph_type_args = mono_targs; + mono->generic_types = gt; + } + mono->generic_parameters = NULL; /* concrete; and persist would free a shared list */ + + /* persist _put_free's these: own live_range/try_catch, drop doc_comment/attributes. */ + if (src->last_live_range) { + mono->live_range = emalloc(sizeof(zend_live_range) * src->last_live_range); + memcpy(mono->live_range, src->live_range, sizeof(zend_live_range) * src->last_live_range); + } else { + mono->live_range = NULL; + } + if (src->last_try_catch) { + mono->try_catch_array = emalloc(sizeof(zend_try_catch_element) * src->last_try_catch); + memcpy(mono->try_catch_array, src->try_catch_array, sizeof(zend_try_catch_element) * src->last_try_catch); + } else { + mono->try_catch_array = NULL; + } + mono->doc_comment = NULL; + mono->attributes = NULL; + + /* Own the dynamic_func_defs array (persist _put_free's it); the closures stay shared (persist xlat-dedups). */ + if (src->num_dynamic_func_defs) { + mono->dynamic_func_defs = emalloc(sizeof(zend_function *) * src->num_dynamic_func_defs); + memcpy(mono->dynamic_func_defs, src->dynamic_func_defs, + sizeof(zend_function *) * src->num_dynamic_func_defs); + } else { + mono->dynamic_func_defs = NULL; + } + + mono->fn_flags2 |= ZEND_ACC2_MONOMORPH_TYPE_ARGS; + + mono->refcount = NULL; + mono->fn_flags &= ~(ZEND_ACC_IMMUTABLE | ZEND_ACC_HEAP_RT_CACHE | ZEND_ACC_PRELOADED); + mono->fn_flags |= ZEND_ACC_TRAIT_CLONE; + mono->function_name = zend_string_copy(src->function_name); + /* Own a filename ref: persist consumes one, and base's is shared with EG(included_files) (else double-free). */ + mono->filename = zend_string_copy(src->filename); + if (new_arg_info) { + mono->arg_info = new_arg_info; + + /* Bake each RECV mask to the concrete arg_info and drop TRAIT_CLONE, only if all RECV params are mask-only (no NAMED). */ + bool can_fast_recv = !(mono->fn_flags & ZEND_ACC_VARIADIC); + for (uint32_t i = 0; can_fast_recv && i < mono->last; i++) { + zend_op *op = &mono->opcodes[i]; + if (op->opcode != ZEND_RECV) { + continue; + } + uint32_t an = op->op1.num; + if (an == 0 || an > mono->num_args) { + can_fast_recv = false; + break; + } + zend_type *t = &new_arg_info[an - 1].type; + if (ZEND_TYPE_IS_SET(*t) && !ZEND_TYPE_IS_ONLY_MASK(*t)) { + can_fast_recv = false; + break; + } + } + if (can_fast_recv) { + for (uint32_t i = 0; i < mono->last; i++) { + zend_op *op = &mono->opcodes[i]; + if (op->opcode == ZEND_RECV) { + op->op2.num = (uint32_t) ZEND_TYPE_FULL_MASK(new_arg_info[op->op1.num - 1].type); + } + } + mono->fn_flags &= ~ZEND_ACC_TRAIT_CLONE; + } + } + + mono->static_variables = src->static_variables + ? zend_array_dup(src->static_variables) : NULL; + /* No run_time_cache slot here: persist assigns it; allocating now corrupts the per-request map_ptr table. */ + ZEND_MAP_PTR_INIT(mono->run_time_cache, NULL); + ZEND_MAP_PTR_INIT(mono->static_variables_ptr, NULL); + + zend_function *mono_fn = (zend_function *) mono; + if (!zend_hash_add_ptr(fn_table, lc_name, mono_fn)) { + existing = zend_hash_find_ptr(fn_table, lc_name); + zend_string_release(display_name); + zend_string_release(lc_name); + return existing ? existing : NULL; + } + + zend_string_release(display_name); + zend_string_release(lc_name); + return mono_fn; +#endif +} + ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name) { size_t lt_pos, args_len; @@ -8038,3 +8253,44 @@ ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_stri efree(args); return mono; } + +ZEND_API zend_function *zend_synthesize_specialized_monomorph_by_name( + HashTable *fn_table, zend_string *name) +{ + /* `name` is case-preserved: lowercase only the base part for the table lookup, parse args as-is. */ + size_t lt_pos, args_len; + if (!zend_mp_split_name(name, <_pos, &args_len)) return NULL; + + zend_string *base_part = zend_string_init(ZSTR_VAL(name), lt_pos, 0); + zend_string *base_lc = zend_string_tolower(base_part); + zend_string_release(base_part); + zend_function *base = zend_hash_find_ptr(fn_table, base_lc); + zend_string_release(base_lc); + if (!base || base->type != ZEND_USER_FUNCTION || !base->op_array.generic_parameters) { + return NULL; + } + + zend_monomorph_parser parser = { + .p = ZSTR_VAL(name) + lt_pos + 1, + .end = ZSTR_VAL(name) + ZSTR_LEN(name) - 1, + .error = false, + }; + uint32_t cap = 4, count = 0; + zend_type *args = emalloc(sizeof(zend_type) * cap); + do { + if (count == cap) { cap *= 2; args = erealloc(args, sizeof(zend_type) * cap); } + args[count++] = zend_mp_parse_type(&parser); + if (parser.error) break; + } while (zend_mp_eat(&parser, ',')); + zend_mp_skip_ws(&parser); + if (parser.error || parser.p != parser.end) { + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return NULL; + } + + zend_function *mono = zend_synthesize_specialized_monomorph_into(fn_table, base, args, count); + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return mono; +} diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index 3e5c6f313146..4e8e648b9fb7 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -126,6 +126,12 @@ ZEND_API bool zend_type_is_reifiable_leaf_composite(zend_type t); ZEND_API zend_function *zend_synthesize_function_monomorph( zend_function *base, const zend_type *args, uint32_t arity); +ZEND_API zend_function *zend_synthesize_specialized_monomorph_into( + HashTable *fn_table, zend_function *base, const zend_type *args, uint32_t arity); + +ZEND_API zend_function *zend_synthesize_specialized_monomorph_by_name( + HashTable *fn_table, zend_string *lc_name); + ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name); void zend_verify_abstract_class(zend_class_entry *ce); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index bc4d718337fe..920590a0cb3b 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -880,8 +880,13 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) * The NEW form of the same opcodes (op1_type != IS_UNUSED) instead caches a * resolved monomorph zend_class_entry* in slot[0]. That entry is owned by * EG(class_table), NOT by the cache slot, so it must be left untouched here - * — freeing it as a type_arg_table corrupts the heap. */ + * — freeing it as a type_arg_table corrupts the heap. + * + * Gate on ZEND_ACC_HEAP_RT_CACHE: SHM/preload op_arrays share the per-request + * map_ptr cache region, already freed by the time they are destroyed — reading + * the stale cache here would be a use-after-free. */ if ((op_array->fn_flags2 & ZEND_ACC2_HAS_GENERIC_CALL_OPS) + && (op_array->fn_flags & ZEND_ACC_HEAP_RT_CACHE) && op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { char *cache_buf = (char *) ZEND_MAP_PTR_GET(op_array->run_time_cache); if (cache_buf) { diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 52bce47d103e..7392a1a7f487 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -5836,10 +5836,14 @@ ZEND_VM_HOT_HANDLER(63, ZEND_RECV, NUM, UNUSED) /* The inline op2.num mask was baked in at the origin's compile time. A * generic-inheritance clone may carry a tighter substituted arg_info that - * the mask doesn't reflect, so force the slow path when the function is a - * clone with substituted types. */ + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + } } ZEND_VM_NEXT_OPCODE(); @@ -5855,11 +5859,14 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_RECV, op->op2.num == MAY_BE_ANY, ZEND_RECV_NO } /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic - * inheritance clone may carry a substituted type in arg_info that we must - * verify against. */ + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { zval *param = EX_VAR(opline->result.var); - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + } } ZEND_VM_NEXT_OPCODE(); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 581b3e8f2a2b..9cf84035d820 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -2908,11 +2908,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_R } /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic - * inheritance clone may carry a substituted type in arg_info that we must - * verify against. */ + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { zval *param = EX_VAR(opline->result.var); - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } } ZEND_VM_NEXT_OPCODE(); @@ -4458,10 +4461,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_R /* The inline op2.num mask was baked in at the origin's compile time. A * generic-inheritance clone may carry a tighter substituted arg_info that - * the mask doesn't reflect, so force the slow path when the function is a - * clone with substituted types. */ + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } } ZEND_VM_NEXT_OPCODE(); @@ -56808,11 +56815,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_RECV_N } /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic - * inheritance clone may carry a substituted type in arg_info that we must - * verify against. */ + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { zval *param = EX_VAR(opline->result.var); - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } } ZEND_VM_NEXT_OPCODE(); @@ -58264,10 +58274,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_RECV_S /* The inline op2.num mask was baked in at the origin's compile time. A * generic-inheritance clone may carry a tighter substituted arg_info that - * the mask doesn't reflect, so force the slow path when the function is a - * clone with substituted types. */ + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } } ZEND_VM_NEXT_OPCODE(); diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 3d005b3835a7..3555158b1da1 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -4371,7 +4371,11 @@ static void preload_fix_trait_op_array(zend_op_array *op_array) } const zend_op_array *orig_op_array = zend_shared_alloc_get_xlat_entry(op_array->refcount); - ZEND_ASSERT(orig_op_array && "Must be in xlat table"); + if (!orig_op_array) { + /* Monomorph clones reuse ZEND_ACC_TRAIT_CLONE but have no original; re-sync would clobber their substituted arg_info. */ + ZEND_ASSERT(zend_class_is_monomorph(op_array->scope)); + return; + } zend_string *function_name = op_array->function_name; zend_class_entry *scope = op_array->scope; @@ -4435,6 +4439,15 @@ static void preload_optimize(zend_persistent_script *script) zend_optimize_script(&script->script, ZCG(accel_directives).optimization_level, ZCG(accel_directives).opt_debug_level); zend_accel_finalize_delayed_early_binding_list(script); + /* Loop to fixpoint over the transitive closure of reachable instantiations. */ + for (int aot_round = 0; aot_round < 32; aot_round++) { + if (zend_aot_monomorphize_script(&script->script, + ZCG(accel_directives).optimization_level) == 0) { + break; + } + } + zend_aot_upgrade_dispatch_to_ucall(&script->script); + ZEND_HASH_MAP_FOREACH_PTR(&script->script.class_table, ce) { preload_fix_trait_methods(ce); } ZEND_HASH_FOREACH_END(); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 5b68782c6ebb..3c41bffd75d2 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -601,7 +601,8 @@ static zend_type_arg_table *zend_persist_type_arg_table( if (!table) { return NULL; } - const zend_type_named_with_args *new_nwa = zend_persist_mono_binding_nwa(ce); + /* ce == NULL: function-level table with concrete owned_type entries, no binding to rebind against. */ + const zend_type_named_with_args *new_nwa = ce ? zend_persist_mono_binding_nwa(ce) : NULL; for (uint32_t i = 0; i < table->count; i++) { if (table->entries[i].name) { zend_accel_store_interned_string(table->entries[i].name); @@ -660,6 +661,11 @@ static zend_generic_type_table *zend_persist_generic_type_table(zend_generic_typ persisted->turbofish_args = zend_persist_turbofish_args_ht(persisted->turbofish_args); } + if (persisted->monomorph_type_args) { + persisted->monomorph_type_args = + zend_persist_type_arg_table(persisted->monomorph_type_args, NULL); + } + /* Precompute the value-check plan into SHM (relocated via the persist arena, * which also works in file_cache mode where no SHM segment is locked). */ if (persisted->parameters) { diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index be1deda11ca7..d4558c16eab9 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -347,6 +347,8 @@ static void zend_persist_turbofish_args_ht_calc(HashTable *ht) ADD_SIZE(sizeof(HashTable)); } +static void zend_persist_type_arg_table_calc(zend_type_arg_table *table); + static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) { if (!table) { @@ -388,6 +390,10 @@ static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) zend_persist_turbofish_args_ht_calc(table->turbofish_args); } + if (table->monomorph_type_args) { + zend_persist_type_arg_table_calc(table->monomorph_type_args); + } + /* Mirror the value-check plan reservation in zend_persist_generic_type_table. */ if (table->parameters) { uint32_t cnt = zend_count_generic_value_checks(table->parameters); From 81c5215cc0d79c93eb2ad31cb16d8d51c4238e7f Mon Sep 17 00:00:00 2001 From: henderkes Date: Sun, 21 Jun 2026 00:18:46 +0000 Subject: [PATCH 30/30] fix CI --- Zend/zend_closures.c | 5 +++ Zend/zend_compile.c | 18 +++++++--- Zend/zend_compile.h | 2 ++ Zend/zend_execute.h | 7 ++++ Zend/zend_inheritance.c | 53 ++++++++++++++++-------------- Zend/zend_opcode.c | 73 +++++++++++++++++++++++++++++++++++++++++ Zend/zend_vm_def.h | 6 ++++ Zend/zend_vm_execute.h | 28 ++++++++++++++++ 8 files changed, 162 insertions(+), 30 deletions(-) diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index c0ceee2ea756..9666fd000205 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -803,6 +803,11 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en if (closure->func.op_array.refcount) { (*closure->func.op_array.refcount)++; } + if ((closure->func.common.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + && closure->func.op_array.generic_types + && closure->func.op_array.generic_types->monomorph_type_args) { + closure->func.op_array.generic_types->monomorph_type_args->refcount++; + } /* For fake closures, we want to reuse the static variables of the original function. */ HashTable *ht = ZEND_MAP_PTR_GET(func->op_array.static_variables_ptr); diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 2adea3d829be..df7831ac1481 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -794,7 +794,8 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( return (zend_type_arg_table *)cache_slot[0]; } zend_type_arg_table *nt = zend_build_generic_call_type_args(call, NULL); - if (nt && nkey && !cache_slot[0]) { + if (nt && nkey && !cache_slot[0] + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE)) { cache_slot[0] = nt; cache_slot[1] = (void *)nkey; nt->persisted = true; @@ -808,7 +809,8 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( } zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); if (t && key && cache_slot && !cache_slot[0] - && zend_call_is_cacheable_against_args(call->func, args_box)) { + && zend_call_is_cacheable_against_args(call->func, args_box) + && (caller->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE)) { cache_slot[0] = t; cache_slot[1] = (void *)key; t->persisted = true; @@ -831,7 +833,8 @@ ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH && cache_slot[3] == (void *) base) { - *out_type_args = (zend_type_arg_table *) cache_slot[4]; + zend_type_arg_table *cached = (zend_type_arg_table *) cache_slot[4]; + *out_type_args = cached ? cached : zend_build_concrete_call_type_args(base, args_box); return (zend_function *) cache_slot[0]; } @@ -861,7 +864,9 @@ ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( } zend_type_arg_table *table = zend_build_concrete_call_type_args(base, args_box); - if (table) { + bool can_cache_table = table && cache_slot && EG(current_execute_data) + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE); + if (can_cache_table) { table->persisted = true; } @@ -869,7 +874,7 @@ ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( cache_slot[0] = mono; cache_slot[1] = (void *) ZEND_TURBOFISH_CACHE_KEY_MONOMORPH; cache_slot[3] = (void *) base; - cache_slot[4] = table; + cache_slot[4] = can_cache_table ? table : NULL; } *out_type_args = table; return mono; @@ -1783,6 +1788,9 @@ static bool zend_try_attach_concrete_call_table_for(zend_op_array *caller, zend_turbofish_args_entry *entry = zend_hash_index_find_ptr(gtt->turbofish_args, args_id); ZEND_ASSERT(entry != NULL); + if (entry->concrete_table && !entry->concrete_table->persisted) { + zend_type_arg_table_destroy(entry->concrete_table); + } entry->concrete_table = ct; entry->concrete_skip_value_check = fbc->op_array.generic_types diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 3781d3cb740e..d05ff5770173 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -261,6 +261,7 @@ typedef struct _zend_type_arg_table { * as part of a persisted class entry's `generic_type_args`. Tells the * destroy path to leave the table and its name/type contents alone. */ bool persisted; + uint32_t refcount; zend_type_arg_entry entries[1]; } zend_type_arg_table; @@ -335,6 +336,7 @@ static zend_always_inline zend_turbofish_args_entry *zend_generic_get_or_cache_a ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count); ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); +ZEND_API void zend_type_arg_table_release(zend_type_arg_table *table); ZEND_API zend_type_arg_table *zend_type_arg_table_capture_clone(const zend_type_arg_table *src); ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index ff9aff51b9d9..815b4917c721 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -345,9 +345,16 @@ static zend_always_inline void zend_vm_init_call_frame(zend_execute_data *call, * call-frame teardown skips destroying it. VERIFY_GENERIC_ARGUMENTS may * still overwrite type_args later for explicit-turbofish calls. */ if (UNEXPECTED(func->common.fn_flags & ZEND_ACC_CLOSURE)) { +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Warray-bounds" +#endif const zend_closure *closure = (const zend_closure *) ZEND_CLOSURE_OBJECT(func); call->type_args = closure->captured_type_args; +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic pop +#endif } else if (UNEXPECTED(ZEND_USER_CODE(func->type) && (func->op_array.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS))) { /* Monomorph reached via by-name dispatch: bind its concrete type-arg table. */ diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index cb68f34cf1d1..8e1a9e292b49 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2939,6 +2939,9 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke clone->hooks = clone_hooks; } + if (ZEND_TYPE_HAS_LIST(sub) || ZEND_TYPE_HAS_NAMED_WITH_ARGS(sub)) { + zend_type_release(sub, /* persistent */ false); + } info = clone; } } @@ -6107,6 +6110,8 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string zval *zv; zend_string *synthesized_lc_parent = NULL; zend_class_entry *cache_key_proto = NULL; + zend_class_name *detached_interface_names = NULL; + uint32_t detached_interface_count = 0; ALLOCA_FLAG(use_heap) SET_ALLOCA_FLAG(use_heap); @@ -6142,10 +6147,12 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string zend_string_addref(ce->interface_names[k].name); zend_string_addref(ce->interface_names[k].lc_name); } + detached_interface_names = ce->interface_names; + detached_interface_count = ce->num_interfaces; } if (ce->num_traits) { zend_class_name *src = ce->trait_names; - ce->trait_names = emalloc(sizeof(zend_class_name) * ce->num_traits); + ce->trait_names = zend_arena_alloc(&CG(arena), sizeof(zend_class_name) * ce->num_traits); memcpy(ce->trait_names, src, sizeof(zend_class_name) * ce->num_traits); for (uint32_t k = 0; k < ce->num_traits; k++) { zend_string_addref(ce->trait_names[k].name); @@ -6230,9 +6237,8 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string ? ce->generic_types->trait_uses : NULL; bool ce_is_mono_for_trait = zend_class_is_monomorph(ce); bool *trait_skip_mono = NULL; - ALLOCA_FLAG(trait_skip_use_heap) if (trait_uses_table_for_synth && ce->num_traits > 1) { - trait_skip_mono = do_alloca(sizeof(bool) * ce->num_traits, trait_skip_use_heap); + trait_skip_mono = emalloc(sizeof(bool) * ce->num_traits); zend_mark_duplicate_lc_names(ce->trait_names, ce->num_traits, trait_skip_mono); } @@ -6257,14 +6263,14 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); if (UNEXPECTED(base_trait == NULL)) { free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } if (UNEXPECTED(!(base_trait->ce_flags & ZEND_ACC_TRAIT))) { zend_throw_error(NULL, "%s cannot use %s - it is not a trait", ZSTR_VAL(ce->name), ZSTR_VAL(base_trait->name)); free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } if (base_trait->generic_parameters) { @@ -6273,7 +6279,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string if (EG(exception)) { check_unrecoverable_load_failure(ce); free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } zend_class_entry *mono = zend_synthesize_monomorph( @@ -6281,7 +6287,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string if (!mono) { check_unrecoverable_load_failure(ce); free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } zend_string_release(ce->trait_names[i].name); @@ -6295,20 +6301,20 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string ce->trait_names[i].lc_name, ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); if (UNEXPECTED(trait == NULL)) { free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } if (UNEXPECTED(!(trait->ce_flags & ZEND_ACC_TRAIT))) { zend_throw_error(NULL, "%s cannot use %s - it is not a trait", ZSTR_VAL(ce->name), ZSTR_VAL(trait->name)); free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } if (UNEXPECTED(trait->ce_flags & ZEND_ACC_DEPRECATED)) { zend_use_of_deprecated_trait(trait, ce->name); if (UNEXPECTED(EG(exception))) { free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } } @@ -6339,7 +6345,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } } if (trait_skip_mono) { - free_alloca(trait_skip_mono, trait_skip_use_heap); + efree(trait_skip_mono); } } @@ -6559,6 +6565,13 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string sizeof(zend_class_entry *) * ce->num_interfaces); zend_do_implement_interfaces(ce, interfaces); + if (detached_interface_names) { + for (uint32_t k = 0; k < detached_interface_count; k++) { + zend_string_release_ex(detached_interface_names[k].name, 0); + zend_string_release_ex(detached_interface_names[k].lc_name, 0); + } + efree(detached_interface_names); + } } else if (parent && parent->num_interfaces) { zend_do_inherit_interfaces(ce, parent); } @@ -7925,17 +7938,12 @@ ZEND_API zend_function *zend_synthesize_function_monomorph( zend_type_arg_table *mono_targs = NULL; { uint32_t tcount = params->count; - mono_targs = zend_arena_alloc(&CG(arena), ZEND_TYPE_ARG_TABLE_SIZE(tcount)); - mono_targs->count = tcount; - mono_targs->generation = 0; + mono_targs = zend_type_arg_table_alloc(tcount); mono_targs->persisted = true; for (uint32_t i = 0; i < tcount; i++) { - mono_targs->entries[i].name = NULL; - mono_targs->entries[i].type_ref = NULL; - mono_targs->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); if (i < arity && ZEND_TYPE_IS_SET(args[i])) { zend_type owned = args[i]; - zend_type_copy_ctor(&owned, /* use_arena */ true, /* persistent */ false); + zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); mono_targs->entries[i].owned_type = owned; zend_string *cname = zend_type_arg_canonical_name(args[i]); mono_targs->entries[i].name = cname; @@ -8079,14 +8087,9 @@ ZEND_API zend_function *zend_synthesize_specialized_monomorph_into( zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(src, args, arity, /* use_arena */ false); - zend_type_arg_table *mono_targs = emalloc(ZEND_TYPE_ARG_TABLE_SIZE(total)); - mono_targs->count = total; - mono_targs->generation = 0; - mono_targs->persisted = false; + zend_type_arg_table *mono_targs = zend_type_arg_table_alloc(total); + mono_targs->persisted = true; for (uint32_t i = 0; i < total; i++) { - mono_targs->entries[i].name = NULL; - mono_targs->entries[i].type_ref = NULL; - mono_targs->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); if (i < arity && ZEND_TYPE_IS_SET(args[i])) { zend_type owned = args[i]; zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 920590a0cb3b..1a7236680d83 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -335,6 +335,7 @@ ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count) { table->count = count; table->generation = zend_type_arg_table_generation_counter++; table->persisted = false; + table->refcount = 1; for (uint32_t i = 0; i < count; i++) { table->entries[i].name = NULL; table->entries[i].type_ref = NULL; @@ -343,6 +344,21 @@ ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count) { return table; } +ZEND_API void zend_type_arg_table_release(zend_type_arg_table *table) { + if (!table || table->refcount == 0 || --table->refcount > 0) { + return; + } + for (uint32_t i = 0; i < table->count; i++) { + if (table->entries[i].name) { + zend_string_release(table->entries[i].name); + } + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_type_release(table->entries[i].owned_type, false); + } + } + efree(table); +} + ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table) { if (!table || table->persisted) { return; @@ -657,6 +673,12 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->num_traits > 0) { _destroy_zend_class_traits_info(ce); } + } else if (ce->num_traits > 0) { + uint32_t i; + for (i = 0; i < ce->num_traits; i++) { + zend_string_release_ex(ce->trait_names[i].name, 0); + zend_string_release_ex(ce->trait_names[i].lc_name, 0); + } } if (ce->default_properties_table) { @@ -698,6 +720,10 @@ ZEND_API void destroy_zend_class(zval *zv) } } } else if (prop_info->flags & ZEND_ACC_GENERIC_CLONE) { + zend_type_release(prop_info->type, /* persistent */ false); + if (prop_info->name) { + zend_string_release_ex(prop_info->name, 0); + } if (prop_info->hooks) { for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { if (prop_info->hooks[i]) { @@ -867,6 +893,36 @@ ZEND_API void zend_destroy_static_vars(zend_op_array *op_array) } } +static void zend_release_transient_monomorph(zend_function *cm) +{ + if (!cm || cm->type != ZEND_USER_FUNCTION + || !(cm->common.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + || (cm->common.fn_flags & ZEND_ACC_IMMUTABLE)) { + return; + } + zend_op_array *moa = &cm->op_array; + if (moa->generic_types && moa->generic_types->monomorph_type_args) { + zend_type_arg_table_release(moa->generic_types->monomorph_type_args); + moa->generic_types->monomorph_type_args = NULL; + } + if (moa->arg_info) { + bool has_ret = (moa->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = moa->num_args + (has_ret ? 1 : 0) + + ((moa->fn_flags & ZEND_ACC_VARIADIC) ? 1 : 0); + zend_arg_info *block = moa->arg_info - (has_ret ? 1 : 0); + for (uint32_t a = 0; a < total; a++) { + zend_type_release(block[a].type, 0); + if (block[a].name) { + zend_string_release(block[a].name); + } + if (block[a].doc_comment) { + zend_string_release(block[a].doc_comment); + } + } + moa->arg_info = NULL; + } +} + ZEND_API void destroy_op_array(zend_op_array *op_array) { uint32_t i; @@ -892,6 +948,14 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) if (cache_buf) { for (uint32_t op_idx = 0; op_idx < op_array->last; op_idx++) { const zend_op *op = &op_array->opcodes[op_idx]; + if ((op->opcode == ZEND_INIT_FCALL_BY_NAME + || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) + && op->result.num) { + void **cache_slot = (void **) (cache_buf + op->result.num); + zend_release_transient_monomorph((zend_function *) cache_slot[0]); + cache_slot[0] = NULL; + continue; + } if ((op->opcode == ZEND_VERIFY_GENERIC_ARGUMENTS || op->opcode == ZEND_INSTALL_GENERIC_ARGS) && op->op1_type == IS_UNUSED @@ -906,6 +970,7 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) zend_type_arg_table_destroy(mt); cache_slot[4] = NULL; } + zend_release_transient_monomorph((zend_function *) cache_slot[0]); cache_slot[0] = NULL; cache_slot[1] = NULL; continue; @@ -931,6 +996,14 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) zend_string_release_ex(op_array->function_name, 0); } + if ((op_array->fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + && !(op_array->fn_flags & ZEND_ACC_IMMUTABLE) + && op_array->generic_types + && op_array->generic_types->monomorph_type_args) { + zend_type_arg_table_release(op_array->generic_types->monomorph_type_args); + op_array->generic_types->monomorph_type_args = NULL; + } + if (!op_array->refcount || --(*op_array->refcount) > 0) { return; } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 7392a1a7f487..9be9d25a2b2b 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4419,6 +4419,8 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER)) } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -9356,6 +9358,10 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 9cf84035d820..e475449738e7 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -1994,6 +1994,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_D } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -2156,6 +2158,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_D } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -2318,6 +2322,8 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -22538,6 +22544,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { @@ -38330,6 +38340,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { @@ -55923,6 +55937,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FCA } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -56085,6 +56101,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FCA } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -56247,6 +56265,8 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FC } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -76149,6 +76169,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { @@ -91941,6 +91965,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) {