Skip to content

Commit 576ce94

Browse files
committed
type aliases
Signed-off-by: Robert Landers <landers.robert@gmail.com>
1 parent 64d8b2a commit 576ce94

File tree

11 files changed

+312
-2
lines changed

11 files changed

+312
-2
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
Basic type alias functionality
3+
--FILE--
4+
<?php
5+
use type int|float as Number;
6+
use type string as Text;
7+
use type ?int as OptionalInt;
8+
use type Number|string as Scalar;
9+
10+
function add(Number $a, Number $b): Number {
11+
return $a + $b;
12+
}
13+
14+
function greet(Text $name): Text {
15+
return "Hello, " . $name;
16+
}
17+
18+
function maybeInt(OptionalInt $value): OptionalInt {
19+
return $value;
20+
}
21+
22+
function identity(Scalar $v): Scalar {
23+
return $v;
24+
}
25+
26+
// Test int|float alias
27+
var_dump(add(1, 2));
28+
var_dump(add(1.5, 2.5));
29+
var_dump(add(1, 2.5));
30+
31+
// Test string alias
32+
var_dump(greet("World"));
33+
34+
// Test nullable alias
35+
var_dump(maybeInt(42));
36+
var_dump(maybeInt(null));
37+
38+
// Test nested alias (Scalar = Number|string = int|float|string)
39+
var_dump(identity(42));
40+
var_dump(identity(3.14));
41+
var_dump(identity("hello"));
42+
43+
echo "Done\n";
44+
?>
45+
--EXPECT--
46+
int(3)
47+
float(4)
48+
float(3.5)
49+
string(12) "Hello, World"
50+
int(42)
51+
NULL
52+
int(42)
53+
float(3.14)
54+
string(5) "hello"
55+
Done
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
include types functionality
3+
--FILE--
4+
<?php
5+
include types 'types.php';
6+
7+
function add(Number $a, Number $b): Number {
8+
return $a + $b;
9+
}
10+
11+
function identity(Scalar $v): Scalar {
12+
return $v;
13+
}
14+
15+
function greet(OptionalString $name): OptionalString {
16+
return $name ? "Hello, " . $name : null;
17+
}
18+
19+
// Test Number alias (int|float)
20+
var_dump(add(1, 2));
21+
var_dump(add(1.5, 2.5));
22+
23+
// Test Scalar alias (Number|string = int|float|string)
24+
var_dump(identity(42));
25+
var_dump(identity(3.14));
26+
var_dump(identity("hello"));
27+
28+
// Test OptionalString alias (?string)
29+
var_dump(greet("World"));
30+
var_dump(greet(null));
31+
32+
echo "Done\n";
33+
?>
34+
--EXPECT--
35+
int(3)
36+
float(4)
37+
int(42)
38+
float(3.14)
39+
string(5) "hello"
40+
string(12) "Hello, World"
41+
NULL
42+
Done
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
--TEST--
2+
include types with invalid file (contains non-type-alias code)
3+
--FILE--
4+
<?php
5+
include types 'invalid_types.php';
6+
?>
7+
--EXPECTF--
8+
Fatal error: include types: Types file 'invalid_types.php' must only contain type aliases (use type ... as ...;) in %s on line %d

Zend/zend_ast.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ enum _zend_ast_kind {
105105
ZEND_AST_LABEL,
106106
ZEND_AST_REF,
107107
ZEND_AST_HALT_COMPILER,
108+
ZEND_AST_INCLUDE_TYPES,
108109
ZEND_AST_ECHO,
109110
ZEND_AST_THROW,
110111
ZEND_AST_GOTO,
@@ -146,6 +147,7 @@ enum _zend_ast_kind {
146147
ZEND_AST_METHOD_REFERENCE,
147148
ZEND_AST_NAMESPACE,
148149
ZEND_AST_USE_ELEM,
150+
ZEND_AST_TYPE_ALIAS,
149151
ZEND_AST_TRAIT_ALIAS,
150152
ZEND_AST_GROUP_USE,
151153
ZEND_AST_ATTRIBUTE,

Zend/zend_compile.c

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,12 @@ static void zend_reset_import_tables(void) /* {{{ */
384384
FC(imports_const) = NULL;
385385
}
386386

387+
if (FC(imports_type)) {
388+
zend_hash_destroy(FC(imports_type));
389+
efree(FC(imports_type));
390+
FC(imports_type) = NULL;
391+
}
392+
387393
zend_hash_clean(&FC(seen_symbols));
388394
}
389395
/* }}} */
@@ -404,6 +410,7 @@ void zend_file_context_begin(zend_file_context *prev_context) /* {{{ */
404410
FC(imports) = NULL;
405411
FC(imports_function) = NULL;
406412
FC(imports_const) = NULL;
413+
FC(imports_type) = NULL;
407414
FC(current_namespace) = NULL;
408415
FC(in_namespace) = 0;
409416
FC(has_bracketed_namespaces) = 0;
@@ -1225,6 +1232,14 @@ static void str_dtor(zval *zv) /* {{{ */ {
12251232
}
12261233
/* }}} */
12271234

1235+
static void ast_ref_dtor(zval *zv) /* {{{ */ {
1236+
/* The AST was created via zend_ast_copy, so we need to destroy the ref */
1237+
zend_ast *ast = Z_PTR_P(zv);
1238+
zend_ast_ref *ref = (zend_ast_ref *)((char *)ast - sizeof(zend_ast_ref));
1239+
zend_ast_ref_destroy(ref);
1240+
}
1241+
/* }}} */
1242+
12281243
static uint32_t zend_add_try_element(uint32_t try_op) /* {{{ */
12291244
{
12301245
zend_op_array *op_array = CG(active_op_array);
@@ -7170,6 +7185,9 @@ ZEND_API void zend_set_function_arg_flags(zend_function *func) /* {{{ */
71707185
}
71717186
/* }}} */
71727187

7188+
/* Forward declaration for type alias support */
7189+
static zend_type zend_compile_typename(zend_ast *ast);
7190+
71737191
static zend_type zend_compile_single_typename(zend_ast *ast)
71747192
{
71757193
ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE));
@@ -7201,6 +7219,17 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
72017219

72027220
return (zend_type) ZEND_TYPE_INIT_CODE(type_code, 0, 0);
72037221
} else {
7222+
/* Check for type alias (only for unqualified names) */
7223+
if (ast->attr == ZEND_NAME_NOT_FQ && FC(imports_type)) {
7224+
zend_string *lookup_name = zend_string_tolower(type_name);
7225+
zend_ast *alias_ast = zend_hash_find_ptr(FC(imports_type), lookup_name);
7226+
zend_string_release_ex(lookup_name, 0);
7227+
if (alias_ast) {
7228+
/* Recursively compile the aliased type */
7229+
return zend_compile_typename(alias_ast);
7230+
}
7231+
}
7232+
72047233
const char *correct_name;
72057234
uint32_t fetch_type = zend_get_class_fetch_type_ast(ast);
72067235
zend_string *class_name = type_name;
@@ -9745,6 +9774,145 @@ static void zend_compile_use(zend_ast *ast) /* {{{ */
97459774
}
97469775
/* }}} */
97479776

9777+
static void zend_compile_use_type_alias(zend_ast *ast) /* {{{ */
9778+
{
9779+
zend_ast *type_ast = ast->child[0];
9780+
zend_ast *alias_ast = ast->child[1];
9781+
zend_string *alias_name = zend_ast_get_str(alias_ast);
9782+
zend_string *lookup_name = zend_string_tolower(alias_name);
9783+
9784+
/* Initialize imports_type hash table if needed */
9785+
if (!FC(imports_type)) {
9786+
FC(imports_type) = emalloc(sizeof(HashTable));
9787+
zend_hash_init(FC(imports_type), 8, NULL, ast_ref_dtor, 0);
9788+
}
9789+
9790+
/* Check if alias is already in use */
9791+
if (zend_hash_exists(FC(imports_type), lookup_name)) {
9792+
zend_error_noreturn(E_COMPILE_ERROR,
9793+
"Cannot use type alias %s because the name is already in use",
9794+
ZSTR_VAL(alias_name));
9795+
}
9796+
9797+
/* Copy the type AST to heap so it can be properly freed */
9798+
zend_ast_ref *ast_ref = zend_ast_copy(type_ast);
9799+
zend_ast *copied_type_ast = GC_AST(ast_ref);
9800+
9801+
/* Store the copied type AST in the hash table */
9802+
zend_hash_add_ptr(FC(imports_type), lookup_name, copied_type_ast);
9803+
9804+
zend_string_release_ex(lookup_name, 0);
9805+
}
9806+
/* }}} */
9807+
9808+
static void zend_compile_include_types(zend_ast *ast) /* {{{ */
9809+
{
9810+
zend_ast *filename_ast = ast->child[0];
9811+
zval *filename_zv = zend_ast_get_zval(filename_ast);
9812+
zend_string *filename = Z_STR_P(filename_zv);
9813+
9814+
/* Resolve the path relative to current file */
9815+
zend_string *resolved_path = zend_resolve_path(filename);
9816+
if (!resolved_path) {
9817+
zend_error_noreturn(E_COMPILE_ERROR,
9818+
"include types: Failed to resolve path '%s'", ZSTR_VAL(filename));
9819+
}
9820+
9821+
/* Open and read the file */
9822+
zend_file_handle file_handle;
9823+
zend_stream_init_filename_ex(&file_handle, resolved_path);
9824+
if (zend_stream_open(&file_handle) == FAILURE) {
9825+
zend_string_release_ex(resolved_path, 0);
9826+
zend_error_noreturn(E_COMPILE_ERROR,
9827+
"include types: Failed to open '%s'", ZSTR_VAL(filename));
9828+
}
9829+
9830+
/* Get file contents */
9831+
char *buf;
9832+
size_t len;
9833+
if (zend_stream_fixup(&file_handle, &buf, &len) == FAILURE) {
9834+
zend_destroy_file_handle(&file_handle);
9835+
zend_string_release_ex(resolved_path, 0);
9836+
zend_error_noreturn(E_COMPILE_ERROR,
9837+
"include types: Failed to read '%s'", ZSTR_VAL(filename));
9838+
}
9839+
9840+
/* Compile to AST */
9841+
zend_string *code = zend_string_init(buf, len, 0);
9842+
zend_arena *ast_arena = NULL;
9843+
zend_ast *types_ast = zend_compile_string_to_ast(code, &ast_arena, resolved_path);
9844+
zend_string_release_ex(code, 0);
9845+
zend_destroy_file_handle(&file_handle);
9846+
9847+
if (!types_ast) {
9848+
zend_string_release_ex(resolved_path, 0);
9849+
zend_error_noreturn(E_COMPILE_ERROR,
9850+
"include types: Failed to parse '%s'", ZSTR_VAL(filename));
9851+
}
9852+
9853+
/* Validate and process the AST - must only contain type aliases */
9854+
if (types_ast->kind != ZEND_AST_STMT_LIST) {
9855+
zend_ast_destroy(types_ast);
9856+
zend_arena_destroy(ast_arena);
9857+
zend_string_release_ex(resolved_path, 0);
9858+
zend_error_noreturn(E_COMPILE_ERROR,
9859+
"include types: Invalid types file '%s'", ZSTR_VAL(filename));
9860+
}
9861+
9862+
zend_ast_list *list = zend_ast_get_list(types_ast);
9863+
for (uint32_t i = 0; i < list->children; i++) {
9864+
zend_ast *stmt = list->child[i];
9865+
if (stmt == NULL) {
9866+
continue;
9867+
}
9868+
if (stmt->kind != ZEND_AST_TYPE_ALIAS) {
9869+
zend_ast_destroy(types_ast);
9870+
zend_arena_destroy(ast_arena);
9871+
zend_string_release_ex(resolved_path, 0);
9872+
zend_error_noreturn(E_COMPILE_ERROR,
9873+
"include types: Types file '%s' must only contain type aliases (use type ... as ...;)",
9874+
ZSTR_VAL(filename));
9875+
}
9876+
9877+
/* Process the type alias - copy AST to current arena */
9878+
zend_ast *type_ast = stmt->child[0];
9879+
zend_ast *alias_ast = stmt->child[1];
9880+
zend_string *alias_name = zend_ast_get_str(alias_ast);
9881+
zend_string *lookup_name = zend_string_tolower(alias_name);
9882+
9883+
/* Initialize imports_type hash table if needed */
9884+
if (!FC(imports_type)) {
9885+
FC(imports_type) = emalloc(sizeof(HashTable));
9886+
zend_hash_init(FC(imports_type), 8, NULL, ast_ref_dtor, 0);
9887+
}
9888+
9889+
/* Check if alias is already in use */
9890+
if (zend_hash_exists(FC(imports_type), lookup_name)) {
9891+
zend_string_release_ex(lookup_name, 0);
9892+
zend_ast_destroy(types_ast);
9893+
zend_arena_destroy(ast_arena);
9894+
zend_string_release_ex(resolved_path, 0);
9895+
zend_error_noreturn(E_COMPILE_ERROR,
9896+
"include types: Cannot use type alias %s because the name is already in use",
9897+
ZSTR_VAL(alias_name));
9898+
}
9899+
9900+
/* Copy the type AST to heap so it survives arena destruction */
9901+
zend_ast_ref *ast_ref = zend_ast_copy(type_ast);
9902+
zend_ast *copied_type_ast = GC_AST(ast_ref);
9903+
9904+
/* Store the copied type AST (the ref keeps it alive) */
9905+
zend_hash_add_ptr(FC(imports_type), lookup_name, copied_type_ast);
9906+
zend_string_release_ex(lookup_name, 0);
9907+
}
9908+
9909+
/* Clean up the types file AST - but the copied ASTs remain in current arena */
9910+
zend_ast_destroy(types_ast);
9911+
zend_arena_destroy(ast_arena);
9912+
zend_string_release_ex(resolved_path, 0);
9913+
}
9914+
/* }}} */
9915+
97489916
static void zend_compile_group_use(const zend_ast *ast) /* {{{ */
97499917
{
97509918
uint32_t i;
@@ -11873,6 +12041,12 @@ static void zend_compile_stmt(zend_ast *ast) /* {{{ */
1187312041
case ZEND_AST_USE:
1187412042
zend_compile_use(ast);
1187512043
break;
12044+
case ZEND_AST_TYPE_ALIAS:
12045+
zend_compile_use_type_alias(ast);
12046+
break;
12047+
case ZEND_AST_INCLUDE_TYPES:
12048+
zend_compile_include_types(ast);
12049+
break;
1187612050
case ZEND_AST_CONST_DECL:
1187712051
zend_compile_const_decl(ast);
1187812052
break;

Zend/zend_compile.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ typedef struct _zend_file_context {
118118
HashTable *imports;
119119
HashTable *imports_function;
120120
HashTable *imports_const;
121+
HashTable *imports_type;
121122

122123
HashTable seen_symbols;
123124
} zend_file_context;

Zend/zend_language_parser.y

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
141141
%token <ident> T_FUNCTION "'function'"
142142
%token <ident> T_FN "'fn'"
143143
%token <ident> T_CONST "'const'"
144+
%token <ident> T_TYPE "'type'"
145+
%token <ident> T_TYPES "'types'"
144146
%token <ident> T_RETURN "'return'"
145147
%token <ident> T_TRY "'try'"
146148
%token <ident> T_CATCH "'catch'"
@@ -309,7 +311,7 @@ reserved_non_modifiers:
309311
| T_INSTANCEOF | T_NEW | T_CLONE | T_EXIT | T_IF | T_ELSEIF | T_ELSE | T_ENDIF | T_ECHO | T_DO | T_WHILE | T_ENDWHILE
310312
| T_FOR | T_ENDFOR | T_FOREACH | T_ENDFOREACH | T_DECLARE | T_ENDDECLARE | T_AS | T_TRY | T_CATCH | T_FINALLY
311313
| T_THROW | T_USE | T_INSTEADOF | T_GLOBAL | T_VAR | T_UNSET | T_ISSET | T_EMPTY | T_CONTINUE | T_GOTO
312-
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT | T_BREAK
314+
| T_FUNCTION | T_CONST | T_TYPE | T_TYPES | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT | T_BREAK
313315
| T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
314316
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_FN | T_MATCH | T_ENUM
315317
| T_PROPERTY_C
@@ -421,6 +423,10 @@ top_statement:
421423
| T_USE use_type group_use_declaration ';' { $$ = $3; $$->attr = $2; }
422424
| T_USE use_declarations ';' { $$ = $2; $$->attr = ZEND_SYMBOL_CLASS; }
423425
| T_USE use_type use_declarations ';' { $$ = $3; $$->attr = $2; }
426+
| T_USE T_TYPE type_expr T_AS T_STRING ';'
427+
{ $$ = zend_ast_create(ZEND_AST_TYPE_ALIAS, $3, $5); }
428+
| T_INCLUDE T_TYPES T_CONSTANT_ENCAPSED_STRING ';'
429+
{ $$ = zend_ast_create(ZEND_AST_INCLUDE_TYPES, $3); }
424430
;
425431

426432
use_type:

Zend/zend_language_scanner.l

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,14 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_
14161416
RETURN_TOKEN_WITH_IDENT(T_CONST);
14171417
}
14181418

1419+
<ST_IN_SCRIPTING>"type" {
1420+
RETURN_TOKEN_WITH_IDENT(T_TYPE);
1421+
}
1422+
1423+
<ST_IN_SCRIPTING>"types" {
1424+
RETURN_TOKEN_WITH_IDENT(T_TYPES);
1425+
}
1426+
14191427
<ST_IN_SCRIPTING>"return" {
14201428
RETURN_TOKEN_WITH_IDENT(T_RETURN);
14211429
}

0 commit comments

Comments
 (0)