Skip to content

Commit 7db71aa

Browse files
authored
Merge pull request #196 from zephir-lang/#195-trait-support
#195 - Add traits support
2 parents f1157d4 + 798907f commit 7db71aa

15 files changed

Lines changed: 374 additions & 15 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [2.1.0] - 2026-07-03
8+
### Added
9+
- Add trait support: top-level `trait Name { }` declarations (AST `type: "trait"`) and in-class `use A, B;` statement. ([zephir-lang/php-zephir-parser#195](https://github.com/zephir-lang/php-zephir-parser/issues/195)).
10+
11+
### Changed
12+
- `trait` is now a reserved keyword; identifiers named `trait` are a syntax error.
13+
714
## [2.0.4] - 2026-06-04
815
### Fixed
916
- A `-` glued to a digit right after a value (e.g. `len-1`, `5-1`, `arr[0]-1`, `len -1`) is now scanned as the binary subtraction operator instead of the sign of a negative literal, which previously produced a `Syntax error`. Unary negative literals (after `=`, `(`, `,`, `return`, etc.) are unchanged, so parameter defaults, constants and array keys keep their negative-literal AST

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.0.4
1+
2.1.0

package.xml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,29 @@
1212
<email>anton@phalcon.io</email>
1313
<active>yes</active>
1414
</lead>
15-
<date>2026-06-04</date>
15+
<date>2026-07-03</date>
1616
<time>10:00:00</time>
1717
<version>
18-
<release>2.0.4</release>
19-
<api>2.0.4</api>
18+
<release>2.1.0</release>
19+
<api>2.1.0</api>
2020
</version>
2121
<stability>
2222
<release>stable</release>
2323
<api>stable</api>
2424
</stability>
2525
<license uri="https://github.com/zephir-lang/php-zephir-parser/blob/development/LICENSE">MIT</license>
2626
<notes>
27-
Thu, Jun 04, 2026 - Zephir Parser 2.0.4
27+
Fri, Jul 03, 2026 - Zephir Parser 2.1.0
2828

29-
= Fixed:
29+
= Added:
3030

31-
- A `-` glued to a digit right after a value (e.g. `len-1`, `5-1`,
32-
`arr[0]-1`, `len -1`) is now scanned as the binary subtraction
33-
operator instead of the sign of a negative literal, which previously
34-
produced a `Syntax error`. Unary negative literals (after `=`, `(`,
35-
`,`, `return`, etc.) are unchanged, so parameter defaults, constants
36-
and array keys keep their negative-literal AST.
31+
- Trait support: top-level `trait Name { }` declarations and in-class
32+
`use A, B;` statements (zephir-lang/zephir#504). Traits accept no
33+
modifiers and cannot extends/implements, matching PHP.
34+
35+
= Changed:
36+
37+
- `trait` is now a reserved keyword.
3738
</notes>
3839
<contents>
3940
<dir name="/">

parser/base.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ void xx_parse_program(zval *return_value, char *program, size_t program_length,
145145
case XX_T_INTERFACE:
146146
xx_(xx_parser, XX_INTERFACE, NULL, parser_status);
147147
break;
148+
case XX_T_TRAIT:
149+
xx_(xx_parser, XX_TRAIT, NULL, parser_status);
150+
break;
148151
case XX_T_EXTENDS:
149152
xx_(xx_parser, XX_EXTENDS, NULL, parser_status);
150153
break;

parser/parser.h

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,41 @@ static void xx_ret_interface(zval *ret, xx_parser_token *T, zval *interface_defi
324324
parser_add_int(ret, "char", state->class_char);
325325
}
326326

327-
static void xx_ret_class_definition(zval *ret, zval *properties, zval *methods, zval *constants, xx_scanner_state *state)
327+
static void xx_ret_trait(zval *ret, xx_parser_token *T, zval *trait_definition, xx_scanner_state *state)
328+
{
329+
array_init(ret);
330+
331+
parser_add_str(ret, "type", "trait");
332+
parser_add_str_free(ret, "name", T->token);
333+
efree(T);
334+
335+
if (trait_definition) {
336+
parser_add_zval(ret, "definition", trait_definition);
337+
}
338+
339+
parser_add_str(ret, "file", state->active_file);
340+
parser_add_int(ret, "line", state->class_line);
341+
parser_add_int(ret, "char", state->class_char);
342+
}
343+
344+
static void xx_ret_use_trait(zval *ret, zval *traits_list, xx_parser_token *D, xx_scanner_state *state)
345+
{
346+
array_init(ret);
347+
348+
parser_add_str(ret, "type", "use-trait");
349+
parser_add_zval(ret, "traits", traits_list);
350+
351+
if (D) {
352+
parser_add_str_free(ret, "docblock", D->token);
353+
efree(D);
354+
}
355+
356+
parser_add_str(ret, "file", state->active_file);
357+
parser_add_int(ret, "line", state->active_line);
358+
parser_add_int(ret, "char", state->active_char);
359+
}
360+
361+
static void xx_ret_class_definition(zval *ret, zval *properties, zval *methods, zval *constants, zval *uses, xx_scanner_state *state)
328362
{
329363
array_init(ret);
330364

@@ -337,6 +371,9 @@ static void xx_ret_class_definition(zval *ret, zval *properties, zval *methods,
337371
if (constants) {
338372
parser_add_zval(ret, "constants", constants);
339373
}
374+
if (uses) {
375+
parser_add_zval(ret, "uses", uses);
376+
}
340377

341378
parser_add_str(ret, "file", state->active_file);
342379
parser_add_int(ret, "line", state->class_line);
@@ -350,13 +387,14 @@ static void xx_ret_class_definition(zval *ret, zval *properties, zval *methods,
350387
*/
351388
static void xx_ret_class_definition_from_list(zval *ret, zval *list, xx_scanner_state *state)
352389
{
353-
zval properties, methods, constants;
390+
zval properties, methods, constants, uses;
354391
zval *member, *type_zval;
355392
const char *type_str;
356393

357394
ZVAL_UNDEF(&properties);
358395
ZVAL_UNDEF(&methods);
359396
ZVAL_UNDEF(&constants);
397+
ZVAL_UNDEF(&uses);
360398

361399
if (list && Z_TYPE_P(list) == IS_ARRAY) {
362400
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(list), member) {
@@ -384,6 +422,11 @@ static void xx_ret_class_definition_from_list(zval *ret, zval *list, xx_scanner_
384422
array_init(&methods);
385423
}
386424
parser_array_append(&methods, member);
425+
} else if (strcmp(type_str, "use-trait") == 0) {
426+
if (Z_TYPE(uses) == IS_UNDEF) {
427+
array_init(&uses);
428+
}
429+
parser_array_append(&uses, member);
387430
} else {
388431
/* Unknown member type — undo the addref */
389432
Z_TRY_DELREF_P(member);
@@ -397,6 +440,7 @@ static void xx_ret_class_definition_from_list(zval *ret, zval *list, xx_scanner_
397440
Z_TYPE(properties) != IS_UNDEF ? &properties : NULL,
398441
Z_TYPE(methods) != IS_UNDEF ? &methods : NULL,
399442
Z_TYPE(constants) != IS_UNDEF ? &constants : NULL,
443+
Z_TYPE(uses) != IS_UNDEF ? &uses : NULL,
400444
state
401445
);
402446
}

parser/scanner.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
#define XX_T_DEPRECATED 399
102102
#define XX_T_REQUIRE_ONCE 459
103103
#define XX_T_YIELD 460
104+
#define XX_T_TRAIT 461
104105

105106
/* Operators */
106107
#define XX_T_AT '@'

parser/scanner.re

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ int xx_get_token(xx_scanner_state *s, xx_scanner_token *token) {
145145
return 0;
146146
}
147147

148+
'trait' {
149+
s->active_char += sizeof("trait")-1;
150+
s->class_line = s->active_line;
151+
s->class_char = s->active_char;
152+
token->opcode = XX_T_TRAIT;
153+
return 0;
154+
}
155+
148156
'extends' {
149157
s->active_char += sizeof("extends")-1;
150158
token->opcode = XX_T_EXTENDS;

parser/zephir.lemon

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ xx_top_statement(R) ::= xx_interface_def(T) . {
147147
R = T;
148148
}
149149

150+
xx_top_statement(R) ::= xx_trait_def(T) . {
151+
R = T;
152+
}
153+
150154
xx_top_statement(R) ::= xx_comment(T) . {
151155
R = T;
152156
}
@@ -315,6 +319,15 @@ xx_interface_def(R) ::= INTERFACE CONSTANT(I) EXTENDS xx_implements_list(L) xx_i
315319
xx_ret_interface(&R, I, &B, &L, status->scanner_state);
316320
}
317321

322+
/* Traits take no modifiers and cannot extend or implement (as in PHP) */
323+
xx_trait_def(R) ::= TRAIT IDENTIFIER(I) xx_class_body(B) . {
324+
xx_ret_trait(&R, I, &B, status->scanner_state);
325+
}
326+
327+
xx_trait_def(R) ::= TRAIT CONSTANT(I) xx_class_body(B) . {
328+
xx_ret_trait(&R, I, &B, status->scanner_state);
329+
}
330+
318331
xx_class_def(R) ::= CLASS IDENTIFIER(I) xx_class_body(B) . {
319332
xx_ret_class(&R, I, &B, 0, 0, NULL, NULL, status->scanner_state);
320333
}
@@ -504,6 +517,19 @@ xx_class_member(R) ::= xx_class_method_definition(M) . {
504517
R = M;
505518
}
506519

520+
xx_class_member(R) ::= xx_use_trait_statement(U) . {
521+
R = U;
522+
}
523+
524+
/* In-class trait use: `use A;` or `use A, B;` — reuses the implements name list */
525+
xx_use_trait_statement(R) ::= USE xx_implements_list(L) DOTCOMMA . {
526+
xx_ret_use_trait(&R, &L, NULL, status->scanner_state);
527+
}
528+
529+
xx_use_trait_statement(R) ::= COMMENT(D) USE xx_implements_list(L) DOTCOMMA . {
530+
xx_ret_use_trait(&R, &L, D, status->scanner_state);
531+
}
532+
507533
/* A flat list of class members in any order */
508534
xx_class_member_list(R) ::= xx_class_member_list(L) xx_class_member(M) . {
509535
xx_ret_list(&R, &L, &M, status->scanner_state);

tests/classes/trait-all-caps.phpt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
Trait declaration with an all-caps name (CONSTANT token) (zephir#504)
3+
--SKIPIF--
4+
<?php include(__DIR__ . '/../skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
$code =<<<ZEP
8+
namespace Test;
9+
10+
trait CSV
11+
{
12+
public function parse() -> int
13+
{
14+
return 1;
15+
}
16+
}
17+
ZEP;
18+
19+
$ir = zephir_parse_file($code, '(eval code)');
20+
$trait = $ir[1];
21+
22+
echo "type: " . $trait["type"] . "\n";
23+
echo "name: " . $trait["name"] . "\n";
24+
echo "methods count: " . count($trait["definition"]["methods"]) . "\n";
25+
26+
$code =<<<ZEP
27+
namespace Test;
28+
29+
trait Empty1
30+
{
31+
}
32+
ZEP;
33+
34+
$ir = zephir_parse_file($code, '(eval code)');
35+
$trait = $ir[1];
36+
37+
echo "type: " . $trait["type"] . "\n";
38+
echo "name: " . $trait["name"] . "\n";
39+
echo "definition set: " . var_export(isset($trait["definition"]), true) . "\n";
40+
?>
41+
--EXPECT--
42+
type: trait
43+
name: CSV
44+
methods count: 1
45+
type: trait
46+
name: Empty1
47+
definition set: false
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
Traits reject modifiers, extends, implements; interfaces reject `use` — syntax errors, as in PHP (zephir#504)
3+
--SKIPIF--
4+
<?php include(__DIR__ . '/../skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
$cases = [
8+
'final trait' => "trait X {\n}\nfinal trait Y {\n}",
9+
'abstract trait' => "abstract trait Y {\n}",
10+
'trait extends' => "trait Y extends Z {\n}",
11+
'trait implements' => "trait Y implements Z {\n}",
12+
'use in interface' => "interface I {\n\tuse T;\n}",
13+
];
14+
15+
foreach ($cases as $label => $zep) {
16+
$ir = zephir_parse_file("namespace Test;\n" . $zep, '(eval code)');
17+
echo $label . ": " . $ir["type"] . "\n";
18+
}
19+
?>
20+
--EXPECT--
21+
final trait: error
22+
abstract trait: error
23+
trait extends: error
24+
trait implements: error
25+
use in interface: error

0 commit comments

Comments
 (0)