Skip to content

Commit f851bcd

Browse files
authored
Merge pull request #192 from zephir-lang/fix-glued-arithmetic-operators
Fix support of glued arithmetic operators
2 parents c45f985 + f34b40e commit f851bcd

8 files changed

Lines changed: 160 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ 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.0.4] - 2026-06-04
8+
### Fixed
9+
- 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
10+
([zephir-lang/zephir#2011](https://github.com/zephir-lang/zephir/issues/2011)).
11+
712
## [2.0.3] - 2026-06-01
813
### Fixed
914
- `instanceof` now binds tighter than logical not, matching PHP — `!a instanceof b` parses as `!(a instanceof b)` instead of `(!a) instanceof b`.

VERSION

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

package.xml

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,28 @@
1212
<email>anton@phalcon.io</email>
1313
<active>yes</active>
1414
</lead>
15-
<date>2026-06-01</date>
15+
<date>2026-06-04</date>
1616
<time>10:00:00</time>
1717
<version>
18-
<release>2.0.3</release>
19-
<api>2.0.3</api>
18+
<release>2.0.4</release>
19+
<api>2.0.4</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-
Mon, Jun 01, 2026 - Zephir Parser 2.0.3
27+
Thu, Jun 04, 2026 - Zephir Parser 2.0.4
2828

2929
= Fixed:
3030

31-
- `instanceof` now binds tighter than logical not, matching PHP:
32-
`!a instanceof b` parses as `!(a instanceof b)` instead of
33-
`(!a) instanceof b`.
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.
3437
</notes>
3538
<contents>
3639
<dir name="/">
@@ -169,6 +172,7 @@
169172
</dir>
170173

171174
<file name="and.phpt" role="test"/>
175+
<file name="arithmetic-unspaced.phpt" role="test"/>
172176
<file name="bitwise.phpt" role="test"/>
173177
<file name="bug89.phpt" role="test"/>
174178
<file name="cast-numeric-types.phpt" role="test"/>
@@ -209,6 +213,7 @@
209213
<file name="special.phpt" role="test"/>
210214
<file name="static-constant-access.phpt" role="test"/>
211215
<file name="static-property-access.phpt" role="test"/>
216+
<file name="sub-unspaced.phpt" role="test"/>
212217
<file name="sub.phpt" role="test"/>
213218
<file name="ternary.phpt" role="test"/>
214219
</dir>

parser/base.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,16 @@ void xx_parse_program(zval *return_value, char *program, size_t program_length,
118118

119119
while (0 <= (scanner_status = xx_get_token(state, &token))) {
120120

121-
state->active_token = token.opcode;
121+
/*
122+
* Keep track of the last *significant* token so the scanner can tell
123+
* whether a '-' glued to a digit is a binary subtraction (it follows a
124+
* value) or part of a negative literal. Whitespace tokens must not
125+
* reset it, otherwise `len -1` would lose the preceding identifier.
126+
* See https://github.com/zephir-lang/zephir/issues/2011
127+
*/
128+
if (token.opcode != XX_T_IGNORE) {
129+
state->active_token = token.opcode;
130+
}
122131
state->start_length = (program + program_length - state->cursor);
123132

124133
switch (token.opcode) {

parser/scanner.re

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,33 @@
2323
#define YYLIMIT (s->limit)
2424
#define YYMARKER (s->marker)
2525

26+
/*
27+
* Returns 1 when the given (previous, significant) token ends a value, in
28+
* which case a following '-' glued to a digit is a binary subtraction operator
29+
* rather than the sign of a negative literal.
30+
*
31+
* See https://github.com/zephir-lang/zephir/issues/2011
32+
*/
33+
static int xx_token_ends_value(int opcode) {
34+
switch (opcode) {
35+
case XX_T_INTEGER:
36+
case XX_T_DOUBLE:
37+
case XX_T_STRING:
38+
case XX_T_ISTRING:
39+
case XX_T_CHAR:
40+
case XX_T_IDENTIFIER:
41+
case XX_T_CONSTANT:
42+
case XX_T_TRUE:
43+
case XX_T_FALSE:
44+
case XX_T_NULL:
45+
case XX_T_PARENTHESES_CLOSE:
46+
case XX_T_SBRACKET_CLOSE:
47+
return 1;
48+
default:
49+
return 0;
50+
}
51+
}
52+
2653
int xx_get_token(xx_scanner_state *s, xx_scanner_token *token) {
2754
char *start = YYCURSOR;
2855
int status = XX_SCANNER_RETCODE_IMPOSSIBLE;
@@ -35,6 +62,17 @@ int xx_get_token(xx_scanner_state *s, xx_scanner_token *token) {
3562
3663
INTEGER = ("-"?[0-9]+)|("-"?[0][x][0-9A-Fa-f]+);
3764
INTEGER {
65+
/*
66+
* A '-' glued to a digit right after a value (e.g. `len-1`) is a
67+
* binary subtraction, not a negative literal: emit SUB and consume
68+
* only the '-'. See zephir issue #2011.
69+
*/
70+
if (start[0] == '-' && xx_token_ends_value(s->active_token)) {
71+
YYCURSOR = start + 1;
72+
s->active_char++;
73+
token->opcode = XX_T_SUB;
74+
return 0;
75+
}
3876
token->opcode = XX_T_INTEGER;
3977
token->value = estrndup(start, YYCURSOR - start);
4078
token->len = YYCURSOR - start;
@@ -44,6 +82,12 @@ int xx_get_token(xx_scanner_state *s, xx_scanner_token *token) {
4482

4583
DOUBLE = ("-"?[0-9]+"."[0-9]+);
4684
DOUBLE {
85+
if (start[0] == '-' && xx_token_ends_value(s->active_token)) {
86+
YYCURSOR = start + 1;
87+
s->active_char++;
88+
token->opcode = XX_T_SUB;
89+
return 0;
90+
}
4791
token->opcode = XX_T_DOUBLE;
4892
token->value = estrndup(start, YYCURSOR - start);
4993
token->len = YYCURSOR - start;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
--TEST--
2+
Arithmetic operators with operands glued to digits, no spaces (zephir #2011)
3+
--SKIPIF--
4+
<?php include(__DIR__ . '/../skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
$code =<<<ZEP
8+
function test() {
9+
let a = b+1;
10+
let a = b-1;
11+
let a = b*1;
12+
let a = b/1;
13+
let a = b%1;
14+
let a = 1+2;
15+
let a = 5-1;
16+
let a = arr[0]*1;
17+
let a = b*-1;
18+
let a = b+-1;
19+
let a = b/-2;
20+
let a = b%-1;
21+
}
22+
ZEP;
23+
24+
function describe(array $e): string
25+
{
26+
if ($e["type"] === "int") {
27+
return "int(" . $e["value"] . ")";
28+
}
29+
return sprintf("%s(%s, %s)", $e["type"], $e["left"]["type"], describe($e["right"]));
30+
}
31+
32+
$ir = zephir_parse_file($code, '(eval code)');
33+
foreach ($ir[0]["statements"] as $statement) {
34+
echo describe($statement["assignments"][0]["expr"]), "\n";
35+
}
36+
?>
37+
--EXPECT--
38+
add(variable, int(1))
39+
sub(variable, int(1))
40+
mul(variable, int(1))
41+
div(variable, int(1))
42+
mod(variable, int(1))
43+
add(int, int(2))
44+
sub(int, int(1))
45+
mul(array-access, int(1))
46+
mul(variable, int(-1))
47+
add(variable, int(-1))
48+
div(variable, int(-2))
49+
mod(variable, int(-1))
50+
--CREDITS--
51+
Zephir Team

tests/operators/sub-unspaced.phpt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
Subtraction with a "-" glued to a digit after a value (zephir #2011)
3+
--SKIPIF--
4+
<?php include(__DIR__ . '/../skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
$code =<<<ZEP
8+
function test() {
9+
let a = b-1;
10+
let a = b -1;
11+
let a = b- 1;
12+
let a = 5-1;
13+
let a = arr[0]-1;
14+
let a = -1;
15+
}
16+
ZEP;
17+
18+
$ir = zephir_parse_file($code, '(eval code)');
19+
foreach ($ir[0]["statements"] as $statement) {
20+
$expr = $statement["assignments"][0]["expr"];
21+
if ($expr["type"] === "int") {
22+
printf("int %s\n", $expr["value"]);
23+
} else {
24+
printf("%s(%s, %s)\n", $expr["type"], $expr["left"]["type"], $expr["right"]["type"]);
25+
}
26+
}
27+
?>
28+
--EXPECT--
29+
sub(variable, int)
30+
sub(variable, int)
31+
sub(variable, int)
32+
sub(int, int)
33+
sub(array-access, int)
34+
int -1
35+
--CREDITS--
36+
Zephir Team

zephir_parser.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extern zend_module_entry zephir_parser_module_entry;
1515
#define phpext_zephir_parser_ptr &zephir_parser_module_entry
1616

1717
#define PHP_ZEPHIR_PARSER_NAME "zephir_parser"
18-
#define PHP_ZEPHIR_PARSER_VERSION "2.0.3"
18+
#define PHP_ZEPHIR_PARSER_VERSION "2.0.4"
1919
#define PHP_ZEPHIR_PARSER_AUTHOR "Zephir Team and contributors"
2020
#define PHP_ZEPHIR_PARSER_DESCRIPTION "The Zephir Parser delivered as a C extension for the PHP language."
2121

0 commit comments

Comments
 (0)