RFC: Pipe operator (#17118)

Co-authored-by: Gina Peter Banyard <girgias@php.net>
Co-authored-by: Arnaud Le Blanc <arnaud.lb@gmail.com>
Co-authored-by: Tim Düsterhus <tim@tideways-gmbh.com>
This commit is contained in:
Larry Garfield 2025-06-10 02:59:43 -05:00 committed by GitHub
parent 2036c7158d
commit 1c09c0c832
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 760 additions and 1 deletions

1
NEWS
View file

@ -54,6 +54,7 @@ PHP NEWS
released on bailout). (DanielEScherzer and ilutov)
. Fixed AST printing for immediately invoked Closure. (Dmitrii Derepko)
. Properly handle __debugInfo() returning an array reference. (nielsdos)
. Added the pipe (|>) operator. (crell)
- Curl:
. Added curl_multi_get_handles(). (timwolla)

View file

@ -144,6 +144,8 @@ PHP 8.5 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/attributes-on-constants
. The #[\Deprecated] attribute can now be used on constants.
RFC: https://wiki.php.net/rfc/attributes-on-constants
. Added the pipe (|>) operator.
RFC: https://wiki.php.net/rfc/pipe-operator-v3
- Curl:
. Added support for share handles that are persisted across multiple PHP
@ -476,6 +478,7 @@ PHP 8.5 UPGRADE NOTES
- Tokenizer:
. T_VOID_CAST.
. T_PIPE.
========================================
11. Changes to INI File Handling

View file

@ -0,0 +1,109 @@
--TEST--
A pipe operator displays as a pipe operator when outputting syntax, with correct parens.
--FILE--
<?php
print "Concat, which binds higher\n";
try {
assert(false && foo() . bar() |> baz() . quux());
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && (foo() . bar()) |> baz() . quux());
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && foo() . (bar() |> baz()) . quux());
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && foo() . bar() |> (baz() . quux()));
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && (foo() . bar() |> baz()) . quux());
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && foo() . (bar() |> baz() . quux()));
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
print "<, which binds lower\n";
try {
assert(false && foo() < bar() |> baz());
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && (foo() < bar()) |> baz());
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && foo() < (bar() |> baz()));
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && foo() |> bar() < baz());
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && (foo() |> bar()) < baz());
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && foo() |> (bar() < baz()));
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
print "misc examples\n";
try {
assert(false && foo() |> (bar() |> baz(...)));
} catch (AssertionError $e) {
echo $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
Concat, which binds higher
assert(false && foo() . bar() |> baz() . quux())
assert(false && foo() . bar() |> baz() . quux())
assert(false && foo() . (bar() |> baz()) . quux())
assert(false && foo() . bar() |> baz() . quux())
assert(false && (foo() . bar() |> baz()) . quux())
assert(false && foo() . (bar() |> baz() . quux()))
<, which binds lower
assert(false && foo() < bar() |> baz())
assert(false && (foo() < bar()) |> baz())
assert(false && foo() < bar() |> baz())
assert(false && foo() |> bar() < baz())
assert(false && foo() |> bar() < baz())
assert(false && foo() |> (bar() < baz()))
misc examples
assert(false && foo() |> (bar() |> baz(...)))

View file

@ -0,0 +1,37 @@
--TEST--
Pipe operator rejects by-reference functions.
--FILE--
<?php
function _modify(int &$a): string {
$a += 1;
return "foo";
}
function _append(array &$a): string {
$a['bar'] = 'beep';
}
// Simple variables
try {
$a = 5;
$res1 = $a |> _modify(...);
var_dump($res1);
} catch (\Error $e) {
echo $e->getMessage(), PHP_EOL;
}
// Complex variables.
try {
$a = ['foo' => 'beep'];
$res2 = $a |> _append(...);
var_dump($res2);
} catch (\Error $e) {
echo $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
_modify(): Argument #1 ($a) could not be passed by reference
_append(): Argument #1 ($a) could not be passed by reference

View file

@ -0,0 +1,17 @@
--TEST--
Pipe operator accepts prefer-by-reference functions.
--FILE--
<?php
$a = ['hello', 'world'];
try {
$r = $a |> array_multisort(...);
var_dump($r);
} catch (\Error $e) {
echo $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
bool(true)

View file

@ -0,0 +1,20 @@
--TEST--
Functions are executed in the expected order
--FILE--
<?php
function foo() { echo __FUNCTION__, PHP_EOL; return 1; }
function bar() { echo __FUNCTION__, PHP_EOL; return false; }
function baz($in) { echo __FUNCTION__, PHP_EOL; return $in; }
function quux($in) { echo __FUNCTION__, PHP_EOL; return $in; }
$result = foo()
|> (bar() ? baz(...) : quux(...))
|> var_dump(...);
?>
--EXPECT--
foo
bar
quux
int(1)

View file

@ -0,0 +1,19 @@
--TEST--
Pipe operator chains
--FILE--
<?php
function _test1(int $a): int {
return $a + 1;
}
function _test2(int $a): int {
return $a * 2;
}
$res1 = 5 |> '_test1' |> '_test2';
var_dump($res1);
?>
--EXPECT--
int(12)

View file

@ -0,0 +1,37 @@
--TEST--
A pipe interrupted by an exception, to demonstrate correct order of execution.
--FILE--
<?php
function foo() { echo __FUNCTION__, PHP_EOL; return 1; }
function bar() { echo __FUNCTION__, PHP_EOL; return false; }
function baz($in) { echo __FUNCTION__, PHP_EOL; return $in; }
function quux($in) { echo __FUNCTION__, PHP_EOL; throw new \Exception('Oops'); }
try {
$result = foo()
|> (bar() ? baz(...) : quux(...))
|> var_dump(...);
}
catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
try {
$result = foo()
|> (throw new Exception('Break'))
|> (bar() ? baz(...) : quux(...))
|> var_dump(...);
}
catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
foo
bar
quux
Exception: Oops
foo
Exception: Break

View file

@ -0,0 +1,15 @@
--TEST--
Pipe operator throws normally on missing function
--FILE--
<?php
try {
$res1 = 5 |> '_test';
}
catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
Error: Call to undefined function _test()

View file

@ -0,0 +1,30 @@
--TEST--
Generators
--FILE--
<?php
function producer(): \Generator {
yield 1;
yield 2;
yield 3;
}
function map_incr(iterable $it): \Generator {
foreach ($it as $val) {
yield $val +1;
}
}
$result = producer() |> map_incr(...) |> iterator_to_array(...);
var_dump($result);
?>
--EXPECT--
array(3) {
[0]=>
int(2)
[1]=>
int(3)
[2]=>
int(4)
}

View file

@ -0,0 +1,80 @@
--TEST--
Pipe operator handles all callable styles
--FILE--
<?php
function times3(int $x): int
{
return $x * 3;
}
function times5(int $x): int
{
return $x * 5;
}
class Test
{
public function times7(int $x): int
{
return $x * 7;
}
public function times11(int $x): int
{
return $x * 11;
}
}
class StaticTest
{
public static function times13(int $x): int
{
return $x * 13;
}
public static function times17(int $x): int
{
return $x * 17;
}
}
function times19(int $x): int
{
return $x * 19;
}
class Times23
{
function __invoke(int $x): int
{
return $x * 23;
}
}
$times29 = function(int $x): int {
return $x * 29;
};
function times2(int $x): int {
return $x * 2;
};
$test = new Test();
$res1 = 1
|> times3(...)
|> 'times5'
|> $test->times7(...)
|> [$test, 'times11']
|> StaticTest::times13(...)
|> [StaticTest::class, 'times17']
|> new Times23()
|> $times29
|> fn($x) => times2($x)
;
var_dump($res1);
?>
--EXPECT--
int(340510170)

View file

@ -0,0 +1,22 @@
--TEST--
Pipe operator handles namespaces
--FILE--
<?php
namespace Beep {
function test(int $x) {
echo $x, PHP_EOL;
}
}
namespace Bar {
use function \Beep\test;
5 |> test(...);
5 |> \Beep\test(...);
}
?>
--EXPECT--
5
5

View file

@ -0,0 +1,89 @@
--TEST--
Pipe operator optimizes away most callables
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.opt_debug_level=0x20000
--EXTENSIONS--
opcache
--FILE--
<?php
function _test1(int $a): int {
return $a + 1;
}
class Other {
public function foo(int $a): int {
return $a * 2;
}
public static function bar(int $a): int {
return $a -1;
}
}
$o = new Other();
$res1 = 5
|> _test1(...)
|> $o->foo(...)
|> Other::bar(...)
;
var_dump($res1);
?>
--EXPECTF--
$_main:
; (lines=18, args=0, vars=2, tmps=2)
; (after optimizer)
; %s:1-27
0000 V2 = NEW 0 string("Other")
0001 DO_FCALL
0002 ASSIGN CV0($o) V2
0003 INIT_FCALL 1 %d string("_test1")
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 V3 = DO_FCALL
0009 T2 = QM_ASSIGN V3
0010 INIT_STATIC_METHOD_CALL 1 string("Other") string("bar")
0011 SEND_VAL T2 1
0012 V2 = DO_UCALL
0013 ASSIGN CV1($res1) V2
0014 INIT_FCALL 1 %d string("var_dump")
0015 SEND_VAR CV1($res1) 1
0016 DO_ICALL
0017 RETURN int(1)
LIVE RANGES:
2: 0001 - 0002 (new)
2: 0010 - 0011 (tmp/var)
_test1:
; (lines=4, args=1, vars=1, tmps=1)
; (after optimizer)
; %s:3-5
0000 CV0($a) = RECV 1
0001 T1 = ADD CV0($a) int(1)
0002 VERIFY_RETURN_TYPE T1
0003 RETURN T1
Other::foo:
; (lines=4, args=1, vars=1, tmps=1)
; (after optimizer)
; %s:8-10
0000 CV0($a) = RECV 1
0001 T1 = ADD CV0($a) CV0($a)
0002 VERIFY_RETURN_TYPE T1
0003 RETURN T1
Other::bar:
; (lines=4, args=1, vars=1, tmps=1)
; (after optimizer)
; %s:12-14
0000 CV0($a) = RECV 1
0001 T1 = SUB CV0($a) int(1)
0002 VERIFY_RETURN_TYPE T1
0003 RETURN T1
int(11)

View file

@ -0,0 +1,15 @@
--TEST--
Pipe operator accepts optional-parameter functions
--FILE--
<?php
function _test(int $a, int $b = 3) {
return $a + $b;
}
$res1 = 5 |> '_test';
var_dump($res1);
?>
--EXPECT--
int(8)

View file

@ -0,0 +1,16 @@
--TEST--
Pipe binds lower than addition
--FILE--
<?php
function _test1(int $a): int {
return $a * 2;
}
// This should add 5+2 first, then pipe that to _test1.
$res1 = 5 + 2 |> '_test1';
var_dump($res1);
?>
--EXPECT--
int(14)

View file

@ -0,0 +1,17 @@
--TEST--
Pipe binds higher than coalesce
--FILE--
<?php
function get_username(int $a): string {
return (string)$a;
}
$user = 5
|> get_username(...)
?? 'default';
var_dump($user);
?>
--EXPECT--
string(1) "5"

View file

@ -0,0 +1,16 @@
--TEST--
Pipe binds higher than comparison
--FILE--
<?php
function _test1(int $a): int {
return $a * 2;
}
// The pipe should run before the comparison.
$res1 = 5 |> _test1(...) == 10 ;
var_dump($res1);
?>
--EXPECTF--
bool(true)

View file

@ -0,0 +1,36 @@
--TEST--
Pipe binds higher than ternary
--FILE--
<?php
function _test1(int $a): int {
return $a + 1;
}
function _test2(int $a): int {
return $a * 2;
}
function _test3(int $a): int {
return $a * 100;
}
function is_odd(int $a): bool {
return (bool)($a % 2);
}
$res1 = 5 |> is_odd(...) ? 'odd' : 'even';
var_dump($res1);
// The pipe binds first, resulting in bool ? int : string, which is well-understood.
$x = true;
$y = 'beep';
$z = 'default';
$ret3 = $x ? $y |> strlen(...) : $z;
var_dump($ret3);
?>
--EXPECT--
string(3) "odd"
int(4)

View file

@ -0,0 +1,11 @@
--TEST--
Pipe operator supports built-in functions
--FILE--
<?php
$res1 = "Hello" |> 'strlen';
var_dump($res1);
?>
--EXPECT--
int(5)

View file

@ -0,0 +1,15 @@
--TEST--
Pipe operator supports user-defined functions
--FILE--
<?php
function _test(int $a): int {
return $a + 1;
}
$res1 = 5 |> '_test';
var_dump($res1);
?>
--EXPECT--
int(6)

View file

@ -0,0 +1,21 @@
--TEST--
Pipe operator fails on multi-parameter functions
--FILE--
<?php
function _test(int $a, int $b) {
return $a + $b;
}
try {
$res1 = 5 |> '_test';
}
catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
ArgumentCountError: Too few arguments to function %s, 1 passed in %s on line %s and exactly 2 expected

View file

@ -0,0 +1,20 @@
--TEST--
Pipe operator respects types
--FILE--
<?php
function _test(int $a, int $b) {
return $a + $b;
}
try {
$res1 = "Hello" |> '_test';
var_dump($res1);
}
catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
TypeError: _test(): Argument #1 ($a) must be of type int, string given, called in %s on line %d

View file

@ -0,0 +1,21 @@
--TEST--
Pipe operator fails void return chaining in strict mode
--FILE--
<?php
declare(strict_types=1);
function nonReturnFunction($bar): void {}
try {
$result = "Hello World"
|> 'nonReturnFunction'
|> 'strlen';
var_dump($result);
}
catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
TypeError: strlen(): Argument #1 ($string) must be of type string, null given

View file

@ -0,0 +1,21 @@
--TEST--
Pipe operator chains saved as a closure
--FILE--
<?php
function _test1(int $a): int {
return $a + 1;
}
function _test2(int $a): int {
return $a * 2;
}
$func = fn($x) => $x |> '_test1' |> '_test2';
$res1 = $func(5);
var_dump($res1);
?>
--EXPECT--
int(12)

View file

@ -2518,6 +2518,7 @@ simple_list:
case ZEND_AST_GREATER_EQUAL: BINARY_OP(" >= ", 180, 181, 181);
case ZEND_AST_AND: BINARY_OP(" && ", 130, 130, 131);
case ZEND_AST_OR: BINARY_OP(" || ", 120, 120, 121);
case ZEND_AST_PIPE: BINARY_OP(" |> ", 183, 183, 184);
case ZEND_AST_ARRAY_ELEM:
if (ast->child[1]) {
zend_ast_export_ex(str, ast->child[1], 80, indent);

View file

@ -154,6 +154,7 @@ enum _zend_ast_kind {
ZEND_AST_MATCH_ARM,
ZEND_AST_NAMED_ARG,
ZEND_AST_PARENT_PROPERTY_HOOK_CALL,
ZEND_AST_PIPE,
/* 3 child nodes */
ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT,

View file

@ -6427,6 +6427,57 @@ static bool can_match_use_jumptable(zend_ast_list *arms) {
return 1;
}
static void zend_compile_pipe(znode *result, zend_ast *ast)
{
zend_ast *operand_ast = ast->child[0];
zend_ast *callable_ast = ast->child[1];
/* Compile the left hand side down to a value first. */
znode operand_result;
zend_compile_expr(&operand_result, operand_ast);
/* Wrap simple values in a ZEND_QM_ASSIGN opcode to ensure references
* always fail. They will already fail in complex cases like arrays,
* so those don't need a wrapper. */
znode wrapped_operand_result;
if (operand_result.op_type & (IS_CV|IS_VAR)) {
zend_emit_op_tmp(&wrapped_operand_result, ZEND_QM_ASSIGN, &operand_result, NULL);
} else {
wrapped_operand_result = operand_result;
}
/* Turn the operand into a function parameter list. */
zend_ast *arg_list_ast = zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create_znode(&wrapped_operand_result));
zend_ast *fcall_ast;
znode callable_result;
/* Turn $foo |> bar(...) into bar($foo). */
if (callable_ast->kind == ZEND_AST_CALL
&& callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT) {
fcall_ast = zend_ast_create(ZEND_AST_CALL,
callable_ast->child[0], arg_list_ast);
/* 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);
/* 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);
/* 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);
}
zend_compile_expr(result, fcall_ast);
}
static void zend_compile_match(znode *result, zend_ast *ast)
{
zend_ast *expr_ast = ast->child[0];
@ -11769,6 +11820,9 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */
case ZEND_AST_MATCH:
zend_compile_match(result, ast);
return;
case ZEND_AST_PIPE:
zend_compile_pipe(result, ast);
return;
default:
ZEND_ASSERT(0 /* not supported */);
}

View file

@ -71,6 +71,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%left T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP
%nonassoc '<' T_IS_SMALLER_OR_EQUAL '>' T_IS_GREATER_OR_EQUAL
%left T_PIPE
%left '.'
%left T_SL T_SR
%left '+' '-'
@ -237,6 +238,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%token T_COALESCE "'??'"
%token T_POW "'**'"
%token T_POW_EQUAL "'**='"
%token T_PIPE "'|>'"
/* We need to split the & token in two to avoid a shift/reduce conflict. For T1&$v and T1&T2,
* with only one token lookahead, bison does not know whether to reduce T1 as a complete type,
* or shift to continue parsing an intersection type. */
@ -1292,6 +1294,8 @@ expr:
{ $$ = zend_ast_create_binary_op(ZEND_IS_EQUAL, $1, $3); }
| expr T_IS_NOT_EQUAL expr
{ $$ = zend_ast_create_binary_op(ZEND_IS_NOT_EQUAL, $1, $3); }
| expr T_PIPE expr
{ $$ = zend_ast_create(ZEND_AST_PIPE, $1, $3); }
| expr '<' expr
{ $$ = zend_ast_create_binary_op(ZEND_IS_SMALLER, $1, $3); }
| expr T_IS_SMALLER_OR_EQUAL expr

View file

@ -1861,6 +1861,10 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_
RETURN_TOKEN(T_COALESCE_EQUAL);
}
<ST_IN_SCRIPTING>"|>" {
RETURN_TOKEN(T_PIPE);
}
<ST_IN_SCRIPTING>"||" {
RETURN_TOKEN(T_BOOLEAN_OR);
}

View file

@ -173,6 +173,7 @@ char *get_token_type_name(int token_type)
case T_COALESCE: return "T_COALESCE";
case T_POW: return "T_POW";
case T_POW_EQUAL: return "T_POW_EQUAL";
case T_PIPE: return "T_PIPE";
case T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG: return "T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG";
case T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG: return "T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG";
case T_BAD_CHARACTER: return "T_BAD_CHARACTER";

View file

@ -742,6 +742,11 @@ const T_POW = UNKNOWN;
* @cvalue T_POW_EQUAL
*/
const T_POW_EQUAL = UNKNOWN;
/**
* @var int
* @cvalue T_PIPE
*/
const T_PIPE = UNKNOWN;
/**
* @var int
* @cvalue T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG

View file

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 19d25d22098f46283b517352cbb302db962b50fd */
* Stub hash: c5235344b7c651d27c2c33c90696a418a9c96837 */
static void register_tokenizer_data_symbols(int module_number)
{
@ -151,6 +151,7 @@ static void register_tokenizer_data_symbols(int module_number)
REGISTER_LONG_CONSTANT("T_COALESCE", T_COALESCE, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_POW", T_POW, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_POW_EQUAL", T_POW_EQUAL, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_PIPE", T_PIPE, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG", T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG", T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_BAD_CHARACTER", T_BAD_CHARACTER, CONST_PERSISTENT);