Skip to content

Commit c2a6cd6

Browse files
authored
Merge pull request #169 from zephir-lang/#16-nested-property-access
#16 - Add support for nested property-access
2 parents 98a353a + 6dec87a commit c2a6cd6

9 files changed

Lines changed: 391 additions & 7 deletions

.github/actions/build-win/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ runs:
4747
.\.ci\lemon-parser.ps1
4848
4949
- name: Setup PHP SDK tool kit
50-
uses: zephir-lang/setup-php-sdk@v1
50+
uses: zephir-lang/setup-php-sdk@main
5151
with:
5252
php_version: ${{ inputs.php_version }}
5353
ts: ${{ inputs.ts }}

parser/parser.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,32 @@ static void xx_ret_let_assignment(zval *ret, char *type, zval *operator, xx_pars
670670
parser_add_int(ret, "char", state->active_char);
671671
}
672672

673+
// New helper supporting nested property access assignments where the base is an expression (e.g. this->arr->arr = 1)
674+
static void xx_ret_let_property_access_assignment(zval *ret, zval *operator, zval *left_expr, xx_parser_token *P, zval *expr, xx_scanner_state *state)
675+
{
676+
array_init(ret);
677+
678+
parser_add_str(ret, "assign-type", "property-access");
679+
if (operator) {
680+
parser_add_zval(ret, "operator", operator);
681+
}
682+
/* Store the left expression chain */
683+
parser_add_zval(ret, "left", left_expr);
684+
685+
if (P) {
686+
parser_add_str_free(ret, "property", P->token);
687+
efree(P);
688+
}
689+
690+
if (expr) {
691+
parser_add_zval(ret, "expr", expr);
692+
}
693+
694+
parser_add_str(ret, "file", state->active_file);
695+
parser_add_int(ret, "line", state->active_line);
696+
parser_add_int(ret, "char", state->active_char);
697+
}
698+
673699
static void xx_ret_if_statement(zval *ret, zval *expr, zval *statements, zval *elseif_statements, zval *else_statements, xx_scanner_state *state)
674700
{
675701
array_init(ret);

parser/zephir.lemon

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
%right BITWISE_NOT .
6666
%right PARENTHESES_CLOSE .
6767
%right SBRACKET_OPEN .
68-
%right ARROW .
68+
%left ARROW .
6969

7070
// The following text is included near the beginning of the C source
7171
// code file that implements the parser.
@@ -1401,6 +1401,10 @@ xx_for_statement(R) ::= FOR IDENTIFIER(K) COMMA IDENTIFIER(V) IN REVERSE xx_comm
14011401
xx_ret_for_statement(&R, &E, K, V, 1, &L, status->scanner_state);
14021402
}
14031403

1404+
xx_for_statement(R) ::= FOR IDENTIFIER(K) COMMA IDENTIFIER(V) IN REVERSE xx_common_expr(E) BRACKET_OPEN BRACKET_CLOSE . {
1405+
xx_ret_for_statement(&R, &E, K, V, 1, NULL, status->scanner_state);
1406+
}
1407+
14041408
xx_for_statement(R) ::= FOR PARENTHESES_OPEN IDENTIFIER(V) IN xx_common_expr(E) PARENTHESES_CLOSE BRACKET_OPEN xx_statement_list(L) BRACKET_CLOSE . {
14051409
xx_ret_for_statement(&R, &E, NULL, V, 0, &L, status->scanner_state);
14061410
}
@@ -1522,15 +1526,39 @@ xx_let_assignment(R) ::= IDENTIFIER(D) ARROW IDENTIFIER(I) SBRACKET_OPEN SBRACKE
15221526
xx_ret_let_assignment(&R, "object-property-append", &O, D, I, NULL, &E, status->scanner_state);
15231527
}
15241528

1525-
/* y->x[z][] = {expr} */
1529+
/* y->x[z] = {expr} */
15261530
xx_let_assignment(R) ::= IDENTIFIER(D) ARROW IDENTIFIER(I) xx_array_offset_list(X) xx_assignment_operator(O) xx_assign_expr(E) . {
15271531
xx_ret_let_assignment(&R, "object-property-array-index", &O, D, I, &X, &E, status->scanner_state);
15281532
}
15291533

1534+
/* y->x[z][] = {expr} */
15301535
xx_let_assignment(R) ::= IDENTIFIER(D) ARROW IDENTIFIER(I) xx_array_offset_list(X) SBRACKET_OPEN SBRACKET_CLOSE xx_assignment_operator(O) xx_assign_expr(E) . {
15311536
xx_ret_let_assignment(&R, "object-property-array-index-append", &O, D, I, &X, &E, status->scanner_state);
15321537
}
15331538

1539+
/* Nested property-access receiver chain (builds a property-access expression chain) */
1540+
xx_let_nested_recv(R) ::= IDENTIFIER(D) ARROW IDENTIFIER(I) . {
1541+
{
1542+
zval id1, id2;
1543+
xx_ret_literal(&id1, XX_T_IDENTIFIER, D, status->scanner_state);
1544+
xx_ret_literal(&id2, XX_T_IDENTIFIER, I, status->scanner_state);
1545+
xx_ret_expr(&R, "property-access", &id1, &id2, NULL, status->scanner_state);
1546+
}
1547+
}
1548+
1549+
xx_let_nested_recv(R) ::= xx_let_nested_recv(L) ARROW IDENTIFIER(I) . {
1550+
{
1551+
zval identifier;
1552+
xx_ret_literal(&identifier, XX_T_IDENTIFIER, I, status->scanner_state);
1553+
xx_ret_expr(&R, "property-access", &L, &identifier, NULL, status->scanner_state);
1554+
}
1555+
}
1556+
1557+
/* {receiver}->prop = {expr} (nested property access assignment, 2+ levels deep) */
1558+
xx_let_assignment(R) ::= xx_let_nested_recv(V) ARROW IDENTIFIER(I) xx_assignment_operator(O) xx_assign_expr(E) . {
1559+
xx_ret_let_property_access_assignment(&R, &O, &V, I, &E, status->scanner_state);
1560+
}
1561+
15341562
/* y::x = {expr} */
15351563
xx_let_assignment(R) ::= IDENTIFIER(D) DOUBLECOLON IDENTIFIER(I) xx_assignment_operator(O) xx_assign_expr(E) . {
15361564
xx_ret_let_assignment(&R, "static-property", &O, D, I, NULL, &E, status->scanner_state);
@@ -1886,7 +1914,7 @@ xx_common_expr(R) ::= xx_common_expr(V) ARROW BRACKET_OPEN STRING(S) BRACKET_CLO
18861914
}
18871915
}
18881916

1889-
xx_common_expr(R) ::= IDENTIFIER(V) DOUBLECOLON IDENTIFIER(I) . {
1917+
xx_common_expr(R) ::= IDENTIFIER(V) DOUBLECOLON IDENTIFIER(I) . [COMMA] {
18901918
{
18911919
zval identifier, identifier2;
18921920
xx_ret_literal(&identifier, XX_T_IDENTIFIER, V, status->scanner_state);
@@ -1994,11 +2022,11 @@ xx_common_expr(R) ::= xx_common_expr(O1) EXCLUSIVE_RANGE xx_common_expr(O2) . {
19942022
}
19952023

19962024
/* y = fetch x, z[k] */
1997-
xx_fetch_expr(R) ::= FETCH IDENTIFIER(O1) COMMA xx_common_expr(O2) . {
2025+
xx_fetch_expr(R) ::= FETCH IDENTIFIER(O1) COMMA xx_common_expr(E2) . {
19982026
{
19992027
zval identifier;
20002028
xx_ret_literal(&identifier, XX_T_IDENTIFIER, O1, status->scanner_state);
2001-
xx_ret_expr(&R, "fetch", &identifier, &O2, NULL, status->scanner_state);
2029+
xx_ret_expr(&R, "fetch", &identifier, &E2, NULL, status->scanner_state);
20022030
}
20032031
}
20042032

@@ -2013,7 +2041,7 @@ xx_common_expr(R) ::= TYPEOF xx_common_expr(O1) . {
20132041
}
20142042

20152043
/* y = x */
2016-
xx_common_expr(R) ::= IDENTIFIER(I) . {
2044+
xx_common_expr(R) ::= IDENTIFIER(I) . [COMMA] {
20172045
xx_ret_literal(&R, XX_T_IDENTIFIER, I, status->scanner_state);
20182046
}
20192047

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
Object property array index assignment (this->items[0] = value)
3+
--SKIPIF--
4+
<?php include(__DIR__ . '/../skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
$code = <<<'ZEP'
8+
namespace Debug;
9+
10+
class Container
11+
{
12+
public items;
13+
14+
public function set()
15+
{
16+
let this->items[0] = "hello";
17+
let this->items["key"] = "world";
18+
}
19+
}
20+
ZEP;
21+
22+
$ir = zephir_parse_file($code, '(eval code)');
23+
$class = $ir[1];
24+
$methods = $class['definition']['methods'];
25+
$set = null;
26+
foreach ($methods as $m) {
27+
if ($m['name'] === 'set') { $set = $m; break; }
28+
}
29+
if (!$set) { echo "MISSING_METHOD\n"; return; }
30+
31+
$statements = $set['statements'];
32+
$lets = [];
33+
foreach ($statements as $st) {
34+
if ($st['type'] === 'let') { $lets[] = $st; }
35+
}
36+
if (count($lets) !== 2) { echo "WRONG_LET_COUNT: " . count($lets) . "\n"; return; }
37+
38+
// First: this->items[0] = "hello"
39+
$a1 = $lets[0]['assignments'][0];
40+
if ($a1['assign-type'] !== 'object-property-array-index') {
41+
echo "TYPE_FAIL_1: " . $a1['assign-type'] . "\n"; return;
42+
}
43+
if ($a1['variable'] !== 'this') { echo "VAR_FAIL_1\n"; return; }
44+
if ($a1['property'] !== 'items') { echo "PROP_FAIL_1\n"; return; }
45+
46+
// Second: this->items["key"] = "world"
47+
$a2 = $lets[1]['assignments'][0];
48+
if ($a2['assign-type'] !== 'object-property-array-index') {
49+
echo "TYPE_FAIL_2: " . $a2['assign-type'] . "\n"; return;
50+
}
51+
52+
echo "OK\n";
53+
?>
54+
--EXPECT--
55+
OK
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
--TEST--
2+
Nested property access with compound assignment operators (this->a->b += 1)
3+
--SKIPIF--
4+
<?php include(__DIR__ . '/../skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
$code = <<<'ZEP'
8+
namespace Debug;
9+
10+
class Chain
11+
{
12+
public a;
13+
14+
public function test()
15+
{
16+
let this->a = new Chain();
17+
let this->a->a += 1;
18+
let this->a->a -= 2;
19+
let this->a->a .= "x";
20+
}
21+
}
22+
ZEP;
23+
24+
$ir = zephir_parse_file($code, '(eval code)');
25+
$class = $ir[1];
26+
$methods = $class['definition']['methods'];
27+
$test = null;
28+
foreach ($methods as $m) {
29+
if ($m['name'] === 'test') { $test = $m; break; }
30+
}
31+
if (!$test) { echo "MISSING_METHOD\n"; return; }
32+
33+
$statements = $test['statements'];
34+
$lets = [];
35+
foreach ($statements as $st) {
36+
if ($st['type'] === 'let') { $lets[] = $st; }
37+
}
38+
if (count($lets) !== 4) { echo "WRONG_LET_COUNT: " . count($lets) . "\n"; return; }
39+
40+
// Second let: this->a->a += 1
41+
$a2 = $lets[1]['assignments'][0];
42+
if ($a2['assign-type'] !== 'property-access') { echo "ASSIGN_TYPE_FAIL: " . $a2['assign-type'] . "\n"; return; }
43+
if ($a2['property'] !== 'a') { echo "PROPERTY_FAIL\n"; return; }
44+
$op2 = $a2['operator'];
45+
if ($op2 !== 'add-assign') { echo "OP2_FAIL: $op2\n"; return; }
46+
47+
// Third let: this->a->a -= 2
48+
$a3 = $lets[2]['assignments'][0];
49+
if ($a3['assign-type'] !== 'property-access') { echo "ASSIGN_TYPE3_FAIL\n"; return; }
50+
$op3 = $a3['operator'];
51+
if ($op3 !== 'sub-assign') { echo "OP3_FAIL: $op3\n"; return; }
52+
53+
// Fourth let: this->a->a .= "x"
54+
$a4 = $lets[3]['assignments'][0];
55+
if ($a4['assign-type'] !== 'property-access') { echo "ASSIGN_TYPE4_FAIL\n"; return; }
56+
$op4 = $a4['operator'];
57+
if ($op4 !== 'concat-assign') { echo "OP4_FAIL: $op4\n"; return; }
58+
59+
echo "OK\n";
60+
?>
61+
--EXPECT--
62+
OK
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
--TEST--
2+
Deep nested property access in let assignment (this->a->b->c = 42)
3+
--SKIPIF--
4+
<?php include(__DIR__ . '/../skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
$code = <<<'ZEP'
8+
namespace Debug;
9+
10+
class Chain
11+
{
12+
public a;
13+
public b;
14+
public c;
15+
16+
public function make()
17+
{
18+
let this->a = new Chain();
19+
let this->a->b = new Chain();
20+
let this->a->b->c = 42;
21+
}
22+
}
23+
ZEP;
24+
25+
$ir = zephir_parse_file($code, '(eval code)');
26+
$class = $ir[1];
27+
$methods = $class['definition']['methods'];
28+
$make = null;
29+
foreach ($methods as $m) {
30+
if ($m['name'] === 'make') { $make = $m; break; }
31+
}
32+
if (!$make) { echo "MISSING_METHOD\n"; return; }
33+
$statements = $make['statements'];
34+
$lets = [];
35+
foreach ($statements as $st) { if ($st['type'] === 'let') { $lets[] = $st; } }
36+
if (count($lets) !== 3) { echo "WRONG_LET_COUNT\n"; return; }
37+
$a1 = $lets[0]['assignments'][0];
38+
$a2 = $lets[1]['assignments'][0];
39+
$a3 = $lets[2]['assignments'][0];
40+
// Validate assign types
41+
if ($a1['assign-type'] !== 'object-property' || $a1['property'] !== 'a') { echo "FIRST_FAIL\n"; return; }
42+
if ($a2['assign-type'] !== 'property-access' || $a2['property'] !== 'b') { echo "SECOND_FAIL\n"; return; }
43+
if ($a3['assign-type'] !== 'property-access' || $a3['property'] !== 'c') { echo "THIRD_FAIL\n"; return; }
44+
// Check left chain forms
45+
if ($a2['left']['type'] !== 'property-access') { echo "CHAIN2_FAIL\n"; return; }
46+
if ($a3['left']['type'] !== 'property-access') { echo "CHAIN3_FAIL\n"; return; }
47+
// Ensure deepest chain left-left structure
48+
$left = $a3['left'];
49+
// $left = property-access for (this->a)->b
50+
// $left['left'] = property-access for this->a
51+
// $left['left']['left'] = variable 'this'
52+
$l2 = $left['left'];
53+
$l3 = $l2['left']; // Should be variable 'this'
54+
if ($left['right']['value'] !== 'b') { echo "B_PROP_MISSING\n"; return; }
55+
if ($l2['right']['value'] !== 'a') { echo "A_PROP_MISSING\n"; return; }
56+
if ($l3['value'] !== 'this') { echo "THIS_MISSING\n"; return; }
57+
echo "OK\n";
58+
?>
59+
--EXPECT--
60+
OK
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--TEST--
2+
Nested property access with method call (this->a->method())
3+
--SKIPIF--
4+
<?php include(__DIR__ . '/../skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
$code = <<<'ZEP'
8+
namespace Debug;
9+
10+
class Caller
11+
{
12+
public child;
13+
14+
public function invoke()
15+
{
16+
this->child->doSomething();
17+
}
18+
}
19+
ZEP;
20+
21+
$ir = zephir_parse_file($code, '(eval code)');
22+
$class = $ir[1];
23+
$methods = $class['definition']['methods'];
24+
$invoke = null;
25+
foreach ($methods as $m) {
26+
if ($m['name'] === 'invoke') { $invoke = $m; break; }
27+
}
28+
if (!$invoke) { echo "MISSING_METHOD\n"; return; }
29+
30+
$statements = $invoke['statements'];
31+
$mcall = null;
32+
foreach ($statements as $st) {
33+
if ($st['type'] === 'mcall') { $mcall = $st; break; }
34+
}
35+
if (!$mcall) { echo "MISSING_MCALL\n"; return; }
36+
37+
// The expr should be an mcall with a property-access variable chain
38+
$expr = $mcall['expr'];
39+
if ($expr['type'] !== 'mcall') { echo "TYPE_FAIL: " . $expr['type'] . "\n"; return; }
40+
if ($expr['name'] !== 'doSomething') { echo "NAME_FAIL: " . $expr['name'] . "\n"; return; }
41+
42+
// The variable (receiver) should be a property-access expression
43+
$variable = $expr['variable'];
44+
if ($variable['type'] !== 'property-access') { echo "VAR_TYPE_FAIL: " . $variable['type'] . "\n"; return; }
45+
46+
echo "OK\n";
47+
?>
48+
--EXPECT--
49+
OK

0 commit comments

Comments
 (0)