From 8f25ea5b53d2df1bba127d11ec9dc1defe6ab3e6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 13:29:01 +0100 Subject: [PATCH 01/25] Make compatible with PHP's pattern matching RFC --- .../ast/syntax/php/IsArrayStructure.class.php | 18 ++ .../lang/ast/syntax/php/IsBinding.class.php | 22 ++ .../ast/syntax/php/IsComparable.class.php | 24 +++ .../lang/ast/syntax/php/IsCompound.class.php | 24 +++ .../syntax/php/IsObjectStructure.class.php | 18 ++ .../lang/ast/syntax/php/IsOperator.class.php | 204 ++++++++++++++---- .../ast/syntax/php/PatternMatch.class.php | 14 ++ .../php/unittest/ArrayStructureTest.class.php | 59 +++++ .../php/unittest/CompoundTest.class.php | 72 +++++++ .../syntax/php/unittest/LiteralTest.class.php | 39 ++++ .../unittest/NumericComparisonTest.class.php | 32 +++ .../unittest/ObjectStructureTest.class.php | 40 ++++ .../ast/syntax/php/unittest/Point.class.php | 38 ++++ .../php/unittest/TypeMatchingTest.class.php | 27 ++- .../unittest/VariableBindingTest.class.php | 37 ++++ .../unittest/VariablePinningTest.class.php | 34 +++ 16 files changed, 651 insertions(+), 51 deletions(-) create mode 100755 src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php create mode 100755 src/main/php/lang/ast/syntax/php/IsBinding.class.php create mode 100755 src/main/php/lang/ast/syntax/php/IsComparable.class.php create mode 100755 src/main/php/lang/ast/syntax/php/IsCompound.class.php create mode 100755 src/main/php/lang/ast/syntax/php/IsObjectStructure.class.php create mode 100755 src/main/php/lang/ast/syntax/php/PatternMatch.class.php create mode 100755 src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php create mode 100755 src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php create mode 100755 src/test/php/lang/ast/syntax/php/unittest/LiteralTest.class.php create mode 100755 src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php create mode 100755 src/test/php/lang/ast/syntax/php/unittest/ObjectStructureTest.class.php create mode 100755 src/test/php/lang/ast/syntax/php/unittest/Point.class.php create mode 100755 src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php create mode 100755 src/test/php/lang/ast/syntax/php/unittest/VariablePinningTest.class.php diff --git a/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php b/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php new file mode 100755 index 0000000..3b231b1 --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php @@ -0,0 +1,18 @@ +patterns= $patterns; + } + + /** @return string */ + public function toString() { + return nameof($this).'('.($this->rest ? '>=' : '===').' '.Objects::stringOf($this->patterns).')'; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/IsBinding.class.php b/src/main/php/lang/ast/syntax/php/IsBinding.class.php new file mode 100755 index 0000000..85c416a --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/IsBinding.class.php @@ -0,0 +1,22 @@ +variable= $variable; + } + + /** @return string */ + public function toString() { + return nameof($this).'('.Objects::stringOf($this->variable).')'; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/IsComparable.class.php b/src/main/php/lang/ast/syntax/php/IsComparable.class.php new file mode 100755 index 0000000..2dbb551 --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/IsComparable.class.php @@ -0,0 +1,24 @@ +value= $value; + $this->operator= $operator; + } + + /** @return string */ + public function toString() { + return nameof($this).'('.$this->operator.' '.Objects::stringOf($this->value).')'; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/IsCompound.class.php b/src/main/php/lang/ast/syntax/php/IsCompound.class.php new file mode 100755 index 0000000..7991134 --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/IsCompound.class.php @@ -0,0 +1,24 @@ +patterns= $patterns; + $this->operator= $operator; + } + + /** @return string */ + public function toString() { + return nameof($this).'('.$this->operator.' '.Objects::stringOf($this->patterns).')'; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/IsObjectStructure.class.php b/src/main/php/lang/ast/syntax/php/IsObjectStructure.class.php new file mode 100755 index 0000000..be9fe3a --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/IsObjectStructure.class.php @@ -0,0 +1,18 @@ +type= $type; + $this->patterns= $patterns; + } + + /** @return string */ + public function toString() { + return nameof($this).'('.Objects::stringOf($this->type).' '.Objects::stringOf($this->patterns).')'; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index d463faf..264ef48 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -1,64 +1,184 @@ infix('is', 60, function($parse, $token, $left) { - $t= $this->type($parse, true); + $pattern= function($parse, $types) use(&$pattern) { + if ('(' === $parse->token->value || '?' === $parse->token->value) { + $r= $types->type0($parse, false); + } else if ('name' === $parse->token->kind) { + $r= $types->type0($parse, false); - $node= new InstanceOfExpression($left, $t ?: $this->expression($parse, 0)); - $node->kind= 'is'; - return $node; - }); + if ('(' === $parse->token->value) { + $r= new IsObjectStructure($r); + $parse->forward(); + if (')' !== $parse->token->value) do { + if (':' === $parse->token->value) { + $parse->forward(); + $variable= $parse->token; + $parse->expecting('(variable)', 'object structure'); + $member= $parse->token->value; + array_unshift($parse->queue, $parse->token); + $parse->token= $variable; + } else { + $member= $parse->token->value; + $parse->forward(); + $parse->expecting(':', 'object structure'); + } + $r->patterns[$member]= $pattern($parse, $types); + } while (',' === $parse->token->value && $parse->forward() | true); + $parse->expecting(')', 'object structure'); + } else if ('::' === $parse->token->value) { + $parse->forward(); + $r= new IsComparable(new ScopeExpression($r->literal(), new Literal($parse->token->value)), '==='); + $parse->forward(); + } + } else if ('string' === $parse->token->kind || 'integer' === $parse->token->kind || 'decimal' === $parse->token->kind) { + $r= new IsComparable(new Literal($parse->token->value), '==='); + $parse->forward(); + } else if ('variable' === $parse->token->kind) { + $parse->forward(); + $r= new IsBinding(new Variable($parse->token->value)); + $parse->forward(); + } else if ('>' === $parse->token->value || '>=' === $parse->token->value || '<' === $parse->token->value || '<=' === $parse->token->value) { + $operator= $parse->token->value; + $parse->forward(); + $r= new IsComparable(new Literal($parse->token->value), $operator); + $parse->forward(); + } else if ('[' === $parse->token->value) { + $r= new IsArrayStructure(); + $parse->forward(); + if (']' !== $parse->token->value) do { + if ('...' === $parse->token->value) { + $r->rest= true; + $parse->forward(); + break; + } - $test= function($literal, $expr, $temp) { - static $is= [ - 'string' => true, - 'int' => true, - 'float' => true, - 'bool' => true, - 'array' => true, - 'object' => true, - 'callable' => true, - 'iterable' => true, - ]; + $p= $pattern($parse, $types); + if ('=>' === $parse->token->value) { + $parse->forward(); + $r->patterns[$p->value->expression]= $pattern($parse, $types); + } else { + $r->patterns[]= $p; + } + } while (',' === $parse->token->value && $parse->forward() | true); + $parse->expecting(']', 'array structure'); + } else if ('^' === $parse->token->value) { + $parse->forward(); + $r= new IsComparable($types->expression($parse, 0), '==='); + } else { + $parse->expecting('a type or literal', 'is'); + return null; + } - if (isset($is[$literal])) { - return new InvokeExpression(new Literal('is_'.$literal), [$expr]); - } else if ('mixed' === $literal) { - return new Literal('true'); + $operator= $parse->token->value; + if ('|' === $operator || '&' === $operator) { + $parse->forward(); + return new IsCompound([$r, $pattern($parse, $types)], $operator); } else { - return new InstanceOfExpression($expr, $literal); + return $r; } }; - $emitter->transform('is', function($codegen, $node) use($test) { - $t= $node->type; - if ($t instanceof Node) { - return new InvokeExpression(new Literal('is'), [$node->type, $node->expression]); - } + $language->infix('is', 60, function($parse, $token, $left) use($pattern) { + return new PatternMatch($left, $pattern($parse, $this), $left->line); + }); - // Verify builtin primitives with is_XXX(), value types with instanceof, others using is() - if ($t instanceof IsFunction || $t instanceof IsArray || $t instanceof IsMap || $t instanceof IsUnion || $t instanceof IsIntersection) { - return new InvokeExpression(new Literal('is'), [new Literal('"'.$t->name().'"'), $node->expression]); - } else { - $literal= $t->literal(); - $temp= new Variable($codegen->symbol()); - if ('?' === $literal[0]) { - return new BinaryExpression( - new BinaryExpression(new Literal('null'), '===', new Braced(new Assignment($temp, '=', $node->expression))), - '||', - $test(substr($literal, 1), $temp, null) - ); + $match= function($codegen, $expression, $pattern) use(&$match) { + // \util\cmd\Console::writeLine('[...] is ', $pattern); + + if ($pattern instanceof IsLiteral) { + $literal= $pattern->literal(); + if ('mixed' === $literal) { + return new Literal('true'); + } else if ('true' === $literal || 'false' === $literal || 'null' === $literal) { + return new BinaryExpression(new Literal($literal), '===', $expression); } else { - return $test($literal, $node->expression, $temp); + return new InvokeExpression(new Literal('is_'.$literal), [$expression]); + } + } else if ($pattern instanceof IsValue) { + return new InstanceOfExpression($expression, $pattern); + } else if ($pattern instanceof IsComparable) { + return new BinaryExpression($expression, $pattern->operator, $pattern->value); + } else if ($pattern instanceof IsNullable) { + $temp= new Variable($codegen->symbol()); + return new BinaryExpression( + new BinaryExpression(new Literal('null'), '===', new Braced(new Assignment($temp, '=', $expression))), + '||', + $match($codegen, $temp, $pattern->element) + ); + } else if ($pattern instanceof IsBinding) { + return new BinaryExpression( + new Literal('true'), + '|', + new Braced(new Assignment($pattern->variable, '=', $expression)) + ); + } else if ($pattern instanceof IsCompound) { + $s= sizeof($pattern->patterns); + if (1 === $s) return $match($codegen, $expression, $pattern->patterns[0]); + + $temp= new Variable($codegen->symbol()); + $compound= $match($codegen, new Braced(new Assignment($temp, '=', $expression)), $pattern->patterns[0]); + for ($i= 1, $op= $pattern->operator.$pattern->operator; $i < $s; $i++) { + $compound= new BinaryExpression($compound, $op, $match($codegen, $temp, $pattern->patterns[$i])); + } + return new Braced($compound); + } else if ($pattern instanceof IsArrayStructure) { + $null= new Literal('null'); + $temp= new Variable($codegen->symbol()); + $compound= new BinaryExpression( + new InvokeExpression(new Literal('is_array'), [new Assignment($temp, '=', $expression)]), + '&&', + new BinaryExpression( + new InvokeExpression(new Literal('sizeof'), [$temp]), + $pattern->rest ? '>=' : '===', + new Literal(sizeof($pattern->patterns)) + ) + ); + foreach ($pattern->patterns as $key => $p) { + $compound= new BinaryExpression($compound, '&&', $match( + $codegen, + new Braced(new BinaryExpression(new OffsetExpression($temp, new Literal($key)), '??', $null)), + $p + )); } + return $compound; + } else if ($pattern instanceof IsObjectStructure) { + $temp= new Variable($codegen->symbol()); + $compound= new InstanceOfExpression(new Braced(new Assignment($temp, '=', $expression)), $pattern->type); + foreach ($pattern->patterns as $key => $p) { + $compound= new BinaryExpression($compound, '&&', $match( + $codegen, + new InstanceExpression($temp, new Literal($key)), + $p + )); + } + return $compound; + } else { + return new InvokeExpression(new Literal('is'), [new Literal('"'.$pattern->name().'"'), $expression]); } + }; + + $emitter->transform('is', function($codegen, $node) use($match) { + return $match($codegen, $node->expression, $node->pattern); }); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/PatternMatch.class.php b/src/main/php/lang/ast/syntax/php/PatternMatch.class.php new file mode 100755 index 0000000..62ac237 --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/PatternMatch.class.php @@ -0,0 +1,14 @@ +expression= $expression; + $this->pattern= $pattern; + $this->line= $line; + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php new file mode 100755 index 0000000..a28ff7c --- /dev/null +++ b/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php @@ -0,0 +1,59 @@ + 1, "two" => 2] is ["one" => 1, "two" => 2]', true]; + yield ['["two" => 2, "one" => 1] is ["one" => 1, "two" => 2]', true]; + yield ['[1, 2] is ["one" => 1, "two" => 2]', false]; + yield ['null is ["one" => 1, "two" => 2]', false]; + + yield ['["one" => 1] is ["one" => 1, ...]', true]; + yield ['["one" => 1, "two" => 2] is ["one" => 1, ...]', true]; + yield ['["two" => 2] is ["one" => 1, ...]', false]; + + yield ['[1, 2] is [1, 2|3]', true]; + yield ['[1, 3] is [1, 2|3]', true]; + yield ['[1, 2, 3] is [1, 2|3]', false]; + yield ['[1, 2] is [1, >=0 & <=10]', true]; + + yield ['[1, 2] is [1, int|string]', true]; + yield ['[1, "2"] is [1, int|string]', true]; + yield ['[1, null] is [1, int|string]', false]; + } + + #[Test, Values(from: 'fixtures')] + public function test($expr, $expected) { + Assert::equals($expected, $this->run('class %T { + public function run() { + return '.$expr.'; + } + }')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php new file mode 100755 index 0000000..572dd65 --- /dev/null +++ b/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php @@ -0,0 +1,72 @@ +0 & <10', true]; + yield ['0 is >0 & <10', false]; + + yield ['1 is int|float', true]; + yield ['1.5 is int|float', true]; + yield ['"test" is int|float', false]; + + yield ['"test" is "success"|"failure"', false]; + yield ['"success" is "success"|"failure"', true]; + yield ['"success" is "success"|"failure"|null', true]; + + yield ['[] is ""|array', true]; + yield ['[] is array|""', true]; + yield ['[] is array|null', true]; + yield ['[] is null|array', true]; + + yield ['$this is array|Traversable', true]; + yield ['$this is IteratorAggregate|Runnable', true]; + yield ['new Date() is IteratorAggregate|Runnable', false]; + + yield ['$this is IteratorAggregate&Runnable', true]; + yield ['null is IteratorAggregate&Runnable', false]; + yield ['new Date() is IteratorAggregate&Runnable', false]; + + yield ['1 is null | >0 & <10', true]; + yield ['null is null | >0 & <10', true]; + yield ['0 is null | >0 & <10', false]; + } + + #[Test, Values(from: 'fixtures')] + public function test($expr, $expected) { + Assert::equals($expected, $this->run(' + use util\\Date, lang\\Runnable, lang\\ast\\syntax\\php\\unittest\\Point; + + class %T implements Runnable, IteratorAggregate { + + public function getIterator(): Traversable { + yield true; + } + + public function run() { + return '.$expr.'; + } + }')); + } + + #[Test] + public function expression_evaluated_once() { + Assert::equals([true, 1], $this->run('class %T { + public function run() { + $evaluated= 0; + $expr= function() use(&$evaluated) { + $evaluated++; + return 1; + }; + + $result= $expr() is >0 & <2; + return [$result, $evaluated]; + } + }')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/LiteralTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/LiteralTest.class.php new file mode 100755 index 0000000..ebeb566 --- /dev/null +++ b/src/test/php/lang/ast/syntax/php/unittest/LiteralTest.class.php @@ -0,0 +1,39 @@ +run('class %T { + const ZERO= 0; + + public function run() { + return '.$expr.'; + } + }')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php new file mode 100755 index 0000000..1908a8c --- /dev/null +++ b/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php @@ -0,0 +1,32 @@ +0', true]; + yield ['1 is >=1', true]; + yield ['1 is <=1', true]; + yield ['0 is <1', true]; + yield ['1 is <1', false]; + + yield ['1.0 is >0.0', true]; + yield ['1.0 is >=1.0', true]; + yield ['1.0 is <=1.0', true]; + yield ['0.0 is <1.0', true]; + yield ['1.0 is <1.0', false]; + } + + #[Test, Values(from: 'fixtures')] + public function test($expr, $expected) { + Assert::equals($expected, $this->run('class %T { + public function run() { + return '.$expr.'; + } + }')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/ObjectStructureTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/ObjectStructureTest.class.php new file mode 100755 index 0000000..afda43b --- /dev/null +++ b/src/test/php/lang/ast/syntax/php/unittest/ObjectStructureTest.class.php @@ -0,0 +1,40 @@ +=0)', true]; + yield ['new Point(1, 2) is Point(x: 1, y: 2|3)', true]; + yield ['new Point(1, 2) is Point(x: 1, y: mixed)', true]; + + yield ['new Point(1, 2) is Point(x: 1)&Value', true]; + yield ['new Point(1, 2) is Point(x: 1)|null', true]; + } + + #[Test, Values(from: 'fixtures')] + public function test($expr, $expected) { + Assert::equals($expected, $this->run('use lang\\Value, lang\\ast\\syntax\\php\\unittest\\Point; class %T { + public function run() { + return '.$expr.'; + } + }')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/Point.class.php b/src/test/php/lang/ast/syntax/php/unittest/Point.class.php new file mode 100755 index 0000000..37df003 --- /dev/null +++ b/src/test/php/lang/ast/syntax/php/unittest/Point.class.php @@ -0,0 +1,38 @@ +x= $x; + $this->y= $y; + $this->z= $z; + } + + /** @return string */ + public function hashCode() { + return 'P'.$this->x.','.$this->y.','.$this->z; + } + + /** @return string */ + public function toString() { + return namoef($this).'('.$this->x.', '.$this->y.', '.$this->z.')'; + } + + /** + * Comparison + * + * @param var $value + * @return int + */ + public function compareTo($value) { + return $value instanceof self + ? Objects::compare([$this->x, $this->y, $this->z], [$value->x, $value->y, $value->z]) + : 1 + ; + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/TypeMatchingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/TypeMatchingTest.class.php index a35f70e..411f9ef 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/TypeMatchingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/TypeMatchingTest.class.php @@ -3,6 +3,7 @@ use lang\ast\unittest\emit\EmittingTest; use test\{Assert, Test, Values}; +/** @see https://wiki.php.net/rfc/pattern-matching#type_pattern */ class TypeMatchingTest extends EmittingTest { /** @return iterable */ @@ -73,20 +74,12 @@ private function fixtures() { yield ['new Date() is Date', true]; yield ['$this is IteratorAggregate', true]; + yield ['$this is Runnable', true]; yield ['$this is Date', false]; - yield ['$this is IteratorAggregate&Runnable', true]; - yield ['new Date() is IteratorAggregate&Runnable', false]; - yield ['null is IteratorAggregate&Runnable', false]; - yield ['$this is self', true]; yield ['new Date() is self', false]; yield ['null is self', false]; - - yield ['1 is int|float', true]; - yield ['1.5 is int|float', true]; - yield ['"test" is int|float', false]; - yield ['$this is array|IteratorAggregate', true]; } #[Test, Values(from: 'fixtures')] @@ -109,4 +102,20 @@ public function run() { } }')); } + + #[Test] + public function expression_evaluated_once() { + Assert::equals([true, 1], $this->run('class %T { + public function run() { + $evaluated= 0; + $expr= function() use(&$evaluated) { + $evaluated++; + return 1; + }; + + $result= $expr() is ?int; + return [$result, $evaluated]; + } + }')); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php new file mode 100755 index 0000000..8409942 --- /dev/null +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -0,0 +1,37 @@ + 0) ? [$x, $y, $z] : null', [1, 2, 3]]; + yield ['new Point(1, 2, 0) is Point(:$x, :$y, :$z & > 0) ? [$x, $y, $z] : null', null]; + + yield ['[1, 2, 3] is [1, 2, $z] ? $z : null', 3]; + yield ['[0, 2, 3] is [1, 2, $z] ? $z : null', null]; + yield ['[1, 2, 3] is [$x, $y, $z] ? [$x, $y, $z] : null', [1, 2, 3]]; + yield ['[0, 1, 2] is [$x, $y, $z] ? [$x, $y, $z] : null', [0, 1, 2]]; + + yield ['["one" => 1, "two" => 2] is ["one" => 1, "two" => $t] ? $t : null', 2]; + yield ['["one" => 0, "two" => 2] is ["one" => 1, "two" => $t] ? $t : null', null]; + } + + #[Test, Values(from: 'fixtures')] + public function test($expr, $expected) { + Assert::equals($expected, $this->run('use lang\\Value, lang\\ast\\syntax\\php\\unittest\\Point; class %T { + public function run() { + return '.$expr.'; + } + }')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariablePinningTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariablePinningTest.class.php new file mode 100755 index 0000000..722fdc4 --- /dev/null +++ b/src/test/php/lang/ast/syntax/php/unittest/VariablePinningTest.class.php @@ -0,0 +1,34 @@ +run('use lang\\Value, lang\\ast\\syntax\\php\\unittest\\Point; class %T { + const ZERO= 0; + + public function run() { + '.$expr.'; + } + }')); + } +} \ No newline at end of file From 93cd1a09b7ba783cf370022b6ac2d48f3b785ca6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 16:34:41 +0100 Subject: [PATCH 02/25] Bump compiler and AST versions --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 86bd48e..7db117f 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ "keywords": ["language", "module", "xp"], "require" : { "xp-framework/core": "^12.0 | ^11.0 | ^10.0", - "xp-framework/compiler": "^9.0 | ^8.0", + "xp-framework/compiler": "^9.0", + "xp-framework/ast": "^11.8", "php" : ">=7.4.0" }, "require-dev" : { From 69a25b7ba2a589117656eac44c841847d31d2288 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 16:39:11 +0100 Subject: [PATCH 03/25] Fix "Trying to access array offset on value of type int" --- src/main/php/lang/ast/syntax/php/IsOperator.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index 264ef48..3bc81f3 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -150,13 +150,13 @@ public function setup($language, $emitter) { new BinaryExpression( new InvokeExpression(new Literal('sizeof'), [$temp]), $pattern->rest ? '>=' : '===', - new Literal(sizeof($pattern->patterns)) + new Literal((string)sizeof($pattern->patterns)) ) ); foreach ($pattern->patterns as $key => $p) { $compound= new BinaryExpression($compound, '&&', $match( $codegen, - new Braced(new BinaryExpression(new OffsetExpression($temp, new Literal($key)), '??', $null)), + new Braced(new BinaryExpression(new OffsetExpression($temp, new Literal((string)$key)), '??', $null)), $p )); } From 89e6dce1b5ddad955582d05e74e03722d20cbb07 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 16:41:21 +0100 Subject: [PATCH 04/25] QA: Apidocs --- .../lang/ast/syntax/php/IsArrayStructure.class.php | 12 +++++++++--- .../lang/ast/syntax/php/IsObjectStructure.class.php | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php b/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php index 3b231b1..75b0ef5 100755 --- a/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php +++ b/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php @@ -4,11 +4,17 @@ use util\Objects; class IsArrayStructure extends Type { - public $patterns; - public $rest= false; + public $patterns, $rest; - public function __construct($patterns= []) { + /** + * Creates a object structure "type" + * + * @param lang.ast.Type[] $patterns + * @param bool $rest + */ + public function __construct($patterns= [], $rest= false) { $this->patterns= $patterns; + $this->rest= $rest; } /** @return string */ diff --git a/src/main/php/lang/ast/syntax/php/IsObjectStructure.class.php b/src/main/php/lang/ast/syntax/php/IsObjectStructure.class.php index be9fe3a..8afdb0a 100755 --- a/src/main/php/lang/ast/syntax/php/IsObjectStructure.class.php +++ b/src/main/php/lang/ast/syntax/php/IsObjectStructure.class.php @@ -6,6 +6,12 @@ class IsObjectStructure extends Type { public $type, $patterns; + /** + * Creates a object structure "type" + * + * @param lang.ast.Type $type + * @param lang.ast.Type[] $patterns + */ public function __construct($type, $patterns= []) { $this->type= $type; $this->patterns= $patterns; From a5c021783638724ffef2d30b1a5f3d10c23d70f9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 16:43:26 +0100 Subject: [PATCH 05/25] Simplify code in last.ast.syntax.php.IsBinding's toString() method --- src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php | 2 +- src/main/php/lang/ast/syntax/php/IsBinding.class.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php b/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php index 75b0ef5..e098ebc 100755 --- a/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php +++ b/src/main/php/lang/ast/syntax/php/IsArrayStructure.class.php @@ -7,7 +7,7 @@ class IsArrayStructure extends Type { public $patterns, $rest; /** - * Creates a object structure "type" + * Creates a array structure "type" * * @param lang.ast.Type[] $patterns * @param bool $rest diff --git a/src/main/php/lang/ast/syntax/php/IsBinding.class.php b/src/main/php/lang/ast/syntax/php/IsBinding.class.php index 85c416a..2d67d7f 100755 --- a/src/main/php/lang/ast/syntax/php/IsBinding.class.php +++ b/src/main/php/lang/ast/syntax/php/IsBinding.class.php @@ -1,7 +1,6 @@ variable= $variable; @@ -17,6 +16,6 @@ public function __construct($variable) { /** @return string */ public function toString() { - return nameof($this).'('.Objects::stringOf($this->variable).')'; + return nameof($this).'('.$this->variable->pointer.')'; } } \ No newline at end of file From ddbcf1deb5714598421de4f488c56f455c2351cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 16:47:12 +0100 Subject: [PATCH 06/25] Refactor: Rename IsComparable -> IsComparison --- .../{IsComparable.class.php => IsComparison.class.php} | 6 +++--- src/main/php/lang/ast/syntax/php/IsOperator.class.php | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) rename src/main/php/lang/ast/syntax/php/{IsComparable.class.php => IsComparison.class.php} (79%) diff --git a/src/main/php/lang/ast/syntax/php/IsComparable.class.php b/src/main/php/lang/ast/syntax/php/IsComparison.class.php similarity index 79% rename from src/main/php/lang/ast/syntax/php/IsComparable.class.php rename to src/main/php/lang/ast/syntax/php/IsComparison.class.php index 2dbb551..b312606 100755 --- a/src/main/php/lang/ast/syntax/php/IsComparable.class.php +++ b/src/main/php/lang/ast/syntax/php/IsComparison.class.php @@ -3,13 +3,13 @@ use lang\ast\Type; use util\Objects; -class IsComparable extends Type { +class IsComparison extends Type { public $value, $operator; /** - * Creates a comparable "type" + * Creates a comparison "type" * - * @param lang.ast.Type[] $patterns + * @param lang.ast.Node $value * @param string $operator */ public function __construct($value, $operator) { diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index 3bc81f3..16fa2d7 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -47,11 +47,11 @@ public function setup($language, $emitter) { $parse->expecting(')', 'object structure'); } else if ('::' === $parse->token->value) { $parse->forward(); - $r= new IsComparable(new ScopeExpression($r->literal(), new Literal($parse->token->value)), '==='); + $r= new IsComparison(new ScopeExpression($r->literal(), new Literal($parse->token->value)), '==='); $parse->forward(); } } else if ('string' === $parse->token->kind || 'integer' === $parse->token->kind || 'decimal' === $parse->token->kind) { - $r= new IsComparable(new Literal($parse->token->value), '==='); + $r= new IsComparison(new Literal($parse->token->value), '==='); $parse->forward(); } else if ('variable' === $parse->token->kind) { $parse->forward(); @@ -60,7 +60,7 @@ public function setup($language, $emitter) { } else if ('>' === $parse->token->value || '>=' === $parse->token->value || '<' === $parse->token->value || '<=' === $parse->token->value) { $operator= $parse->token->value; $parse->forward(); - $r= new IsComparable(new Literal($parse->token->value), $operator); + $r= new IsComparison(new Literal($parse->token->value), $operator); $parse->forward(); } else if ('[' === $parse->token->value) { $r= new IsArrayStructure(); @@ -83,7 +83,7 @@ public function setup($language, $emitter) { $parse->expecting(']', 'array structure'); } else if ('^' === $parse->token->value) { $parse->forward(); - $r= new IsComparable($types->expression($parse, 0), '==='); + $r= new IsComparison($types->expression($parse, 0), '==='); } else { $parse->expecting('a type or literal', 'is'); return null; @@ -116,7 +116,7 @@ public function setup($language, $emitter) { } } else if ($pattern instanceof IsValue) { return new InstanceOfExpression($expression, $pattern); - } else if ($pattern instanceof IsComparable) { + } else if ($pattern instanceof IsComparison) { return new BinaryExpression($expression, $pattern->operator, $pattern->value); } else if ($pattern instanceof IsNullable) { $temp= new Variable($codegen->symbol()); From c76a39c3ca9745451a177ed6b5c9ece3aaa13ce8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 17:09:18 +0100 Subject: [PATCH 07/25] Support patterns in match() expressions --- .../lang/ast/syntax/php/IsOperator.class.php | 54 +++++++++++++++++-- .../php/unittest/IsOperatorTest.class.php | 14 +++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index 16fa2d7..0506e5e 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -1,21 +1,23 @@ line); }); + $language->prefix('match', 0, function($parse, $token) use($pattern) { + $patterns= false; + $condition= null; + + if ('(' === $parse->token->value) { + $parse->forward(); + $condition= $this->expression($parse, 0); + $parse->expecting(')', 'match'); + + // See https://wiki.php.net/rfc/pattern-matching#match_is_placement + if ('is' === $parse->token->value) { + $parse->forward(); + $patterns= true; + } + } + + $default= null; + $cases= []; + $parse->expecting('{', 'match'); + while ('}' !== $parse->token->value) { + if ('default' === $parse->token->value) { + $parse->forward(); + $parse->expecting('=>', 'match'); + $default= $this->expression($parse, 0); + } else if ($patterns) { + $match= [new PatternMatch($condition, $pattern($parse, $this), $parse->token->line)]; + $parse->expecting('=>', 'match'); + $cases[]= new MatchCondition($match, $this->expression($parse, 0), $parse->token->line); + } else { + $match= []; + do { + $match[]= $this->expression($parse, 0); + } while (',' === $parse->token->value && $parse->forward() | true); + $parse->expecting('=>', 'match'); + $cases[]= new MatchCondition($match, $this->expression($parse, 0), $parse->token->line); + } + + if (',' === $parse->token->value) { + $parse->forward(); + } + } + $parse->expecting('}', 'match'); + + return new MatchExpression($patterns ? null : $condition, $cases, $default, $token->line); + }); + $match= function($codegen, $expression, $pattern) use(&$match) { // \util\cmd\Console::writeLine('[...] is ', $pattern); diff --git a/src/test/php/lang/ast/syntax/php/unittest/IsOperatorTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/IsOperatorTest.class.php index 9b28b66..6e2bdd9 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/IsOperatorTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/IsOperatorTest.class.php @@ -41,4 +41,18 @@ public function run(string|int $arg) { Assert::equals($expected, $r); } + + #[Test, Values([[1, 'int'], ['test', 'string']])] + public function match_is_variant($arg, $expected) { + $r= $this->run('class %T { + public function run(string|int $arg) { + return match ($arg) is { + string => "string", + int => "int", + }; + } + }', $arg); + + Assert::equals($expected, $r); + } } \ No newline at end of file From 33899a0918c884d9076b18d1864e01479d24b290 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 19:23:28 +0100 Subject: [PATCH 08/25] Merge compound types with the same operators --- .../php/lang/ast/syntax/php/IsOperator.class.php | 16 ++++++++++++++-- .../syntax/php/unittest/CompoundTest.class.php | 13 +++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index 0506e5e..8c1c35d 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -23,7 +23,11 @@ class IsOperator implements Extension { public function setup($language, $emitter) { $pattern= function($parse, $types) use(&$pattern) { - if ('(' === $parse->token->value || '?' === $parse->token->value) { + if ('(' === $parse->token->value) { + $parse->forward(); + $r= $pattern($parse, $types); + $parse->expecting(')', 'dnf'); + } else if ('?' === $parse->token->value) { $r= $types->type0($parse, false); } else if ('name' === $parse->token->kind) { $r= $types->type0($parse, false); @@ -94,7 +98,15 @@ public function setup($language, $emitter) { $operator= $parse->token->value; if ('|' === $operator || '&' === $operator) { $parse->forward(); - return new IsCompound([$r, $pattern($parse, $types)], $operator); + $n= $pattern($parse, $types); + + // Merge compound types with the same operators, keeping evaluation order + if ($n instanceof IsCompound && $operator === $n->operator) { + array_unshift($n->patterns, $r); + return $n; + } else { + return new IsCompound([$r, $n], $operator); + } } else { return $r; } diff --git a/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php index 572dd65..e5200f6 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php @@ -15,9 +15,15 @@ private function fixtures() { yield ['1.5 is int|float', true]; yield ['"test" is int|float', false]; + yield ['1 is int & 1', true]; + yield ['2 is int & (1|2)', true]; + yield ['3 is int & float', false]; + yield ['"test" is string & "success"', false]; + yield ['"test" is "success"|"failure"', false]; yield ['"success" is "success"|"failure"', true]; yield ['"success" is "success"|"failure"|null', true]; + yield ['"success" is "success"|"failure"|"running"', true]; yield ['[] is ""|array', true]; yield ['[] is array|""', true]; @@ -29,9 +35,16 @@ private function fixtures() { yield ['new Date() is IteratorAggregate|Runnable', false]; yield ['$this is IteratorAggregate&Runnable', true]; + yield ['$this is (IteratorAggregate&Runnable)', true]; yield ['null is IteratorAggregate&Runnable', false]; yield ['new Date() is IteratorAggregate&Runnable', false]; + yield ['$this is null|(IteratorAggregate&Runnable)', true]; + yield ['null is null|(IteratorAggregate&Runnable)', true]; + yield ['$this is (IteratorAggregate&Runnable)|null', true]; + yield ['null is (IteratorAggregate&Runnable)|null', true]; + yield ['null is IteratorAggregate&(Runnable|null)', false]; + yield ['1 is null | >0 & <10', true]; yield ['null is null | >0 & <10', true]; yield ['0 is null | >0 & <10', false]; From b4c2b721c67c1500749554d05f9d13302005bba6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 19:42:31 +0100 Subject: [PATCH 09/25] Verify input for numeric comparison with is_numeric() --- .../lang/ast/syntax/php/IsIdentical.class.php | 22 +++++++++++++++++++ .../lang/ast/syntax/php/IsOperator.class.php | 14 ++++++++++-- .../php/unittest/ArrayStructureTest.class.php | 6 +++++ .../php/unittest/CompoundTest.class.php | 1 + .../unittest/NumericComparisonTest.class.php | 9 ++++++++ .../unittest/VariableBindingTest.class.php | 5 +++++ 6 files changed, 55 insertions(+), 2 deletions(-) create mode 100755 src/main/php/lang/ast/syntax/php/IsIdentical.class.php diff --git a/src/main/php/lang/ast/syntax/php/IsIdentical.class.php b/src/main/php/lang/ast/syntax/php/IsIdentical.class.php new file mode 100755 index 0000000..7c743c4 --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/IsIdentical.class.php @@ -0,0 +1,22 @@ +value= $value; + } + + /** @return string */ + public function toString() { + return nameof($this).'('.Objects::stringOf($this->value).')'; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index 8c1c35d..e99a15f 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -57,7 +57,7 @@ public function setup($language, $emitter) { $parse->forward(); } } else if ('string' === $parse->token->kind || 'integer' === $parse->token->kind || 'decimal' === $parse->token->kind) { - $r= new IsComparison(new Literal($parse->token->value), '==='); + $r= new IsIdentical(new Literal($parse->token->value)); $parse->forward(); } else if ('variable' === $parse->token->kind) { $parse->forward(); @@ -176,8 +176,18 @@ public function setup($language, $emitter) { } } else if ($pattern instanceof IsValue) { return new InstanceOfExpression($expression, $pattern); + } else if ($pattern instanceof IsIdentical) { + return new BinaryExpression($expression, '===', $pattern->value); } else if ($pattern instanceof IsComparison) { - return new BinaryExpression($expression, $pattern->operator, $pattern->value); + $temp= new Variable($codegen->symbol()); + return new BinaryExpression( + new InvokeExpression( + new Literal('is_numeric'), + [new Braced(new Assignment($temp, '=', $expression))] + ), + '&&', + new BinaryExpression($temp, $pattern->operator, $pattern->value) + ); } else if ($pattern instanceof IsNullable) { $temp= new Variable($codegen->symbol()); return new BinaryExpression( diff --git a/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php index a28ff7c..58e82c1 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php @@ -24,6 +24,7 @@ private function fixtures() { yield ['[] is [1, 2]', false]; yield ['null is [1, 2]', false]; + yield ['[1] is [...]', true]; yield ['[1, 2] is [...]', true]; yield ['[1, 2] is [1, 2, ...]', true]; yield ['[1, 2, 3] is [1, 2, ...]', true]; @@ -38,6 +39,11 @@ private function fixtures() { yield ['["one" => 1, "two" => 2] is ["one" => 1, ...]', true]; yield ['["two" => 2] is ["one" => 1, ...]', false]; + yield ['[2] is [0 => 2]', true]; + yield ['[2] is ["0" => 2]', true]; + yield ['[1] is [0 => 2]', false]; + yield ['[1] is ["0" => 2]', false]; + yield ['[1, 2] is [1, 2|3]', true]; yield ['[1, 3] is [1, 2|3]', true]; yield ['[1, 2, 3] is [1, 2|3]', false]; diff --git a/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php index e5200f6..e14b590 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php @@ -29,6 +29,7 @@ private function fixtures() { yield ['[] is array|""', true]; yield ['[] is array|null', true]; yield ['[] is null|array', true]; + yield ['[] is [] & [...]', true]; yield ['$this is array|Traversable', true]; yield ['$this is IteratorAggregate|Runnable', true]; diff --git a/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php index 1908a8c..cf5b1b2 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php @@ -19,6 +19,15 @@ private function fixtures() { yield ['1.0 is <=1.0', true]; yield ['0.0 is <1.0', true]; yield ['1.0 is <1.0', false]; + + yield ['"1" is >0', true]; + yield ['"1e2" is >=100', true]; + yield ['"3.141" is >3 & <4', true]; + + yield ['null is >0', false]; + yield ['[] is >0', false]; + yield ['"test" is >0', false]; + yield ['((object)[]) is >0', false]; } #[Test, Values(from: 'fixtures')] diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index 8409942..7a21ac7 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -8,12 +8,17 @@ class VariableBindingTest extends EmittingTest { /** @return iterable */ private function fixtures() { + yield ['new Point(1, 2, 3) is Point(z: $z) ? $z : null', 3]; + yield ['new Point(1, 2, 3) is Point(:$z) ? $z : null', 3]; + yield ['new Point(1, 2, 3) is Point(z: $bound) ? $bound : null', 3]; + yield ['new Point(1, 2, 3) is Point(x: 1, y: 2, z: $z) ? $z : null', 3]; yield ['new Point(1, 2, 3) is Point(x: 0, y: 2, z: $z) ? $z : null', null]; yield ['new Point(1, 2, 3) is Point(x: $x, y: $y, z: $z) ? [$x, $y, $z] : null', [1, 2, 3]]; yield ['new Point(1, 2, 3) is Point(:$x, :$y, :$z) ? [$x, $y, $z] : null', [1, 2, 3]]; + yield ['new Point(1, 2, 3) is Point(:$z & ?int) ? $z : null', 3]; yield ['new Point(1, 2, 3) is Point(:$x, :$y, :$z & > 0) ? [$x, $y, $z] : null', [1, 2, 3]]; yield ['new Point(1, 2, 0) is Point(:$x, :$y, :$z & > 0) ? [$x, $y, $z] : null', null]; From bad13af72d7ca537a98f161a3d65be8f603db2b6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 19:54:36 +0100 Subject: [PATCH 10/25] Test delayed binding --- .../php/unittest/VariableBindingTest.class.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index 7a21ac7..874cb91 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -1,7 +1,8 @@ run('use lang\\ast\\syntax\\php\\unittest\\Point; class %T { + public function run() { + new Point(1, -1) is Point(x: 0, :$z); + return $z; + } + }')); + } } \ No newline at end of file From 2bcf0fc495061cc8fcba75566e907e6841b8b0cd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 19:54:47 +0100 Subject: [PATCH 11/25] Add tests with global constants --- .../php/lang/ast/syntax/php/unittest/CompoundTest.class.php | 1 + .../ast/syntax/php/unittest/NumericComparisonTest.class.php | 3 +++ .../lang/ast/syntax/php/unittest/VariablePinningTest.class.php | 1 + 3 files changed, 5 insertions(+) diff --git a/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php index e14b590..c4af735 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/CompoundTest.class.php @@ -18,6 +18,7 @@ private function fixtures() { yield ['1 is int & 1', true]; yield ['2 is int & (1|2)', true]; yield ['3 is int & float', false]; + yield ['3 is 1|2|3', true]; yield ['"test" is string & "success"', false]; yield ['"test" is "success"|"failure"', false]; diff --git a/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php index cf5b1b2..4d351b6 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/NumericComparisonTest.class.php @@ -20,6 +20,9 @@ private function fixtures() { yield ['0.0 is <1.0', true]; yield ['1.0 is <1.0', false]; + yield ['0 is 0', true]; + yield ['"1" is >0', true]; yield ['"1e2" is >=100', true]; yield ['"3.141" is >3 & <4', true]; diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariablePinningTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariablePinningTest.class.php index 722fdc4..76a8fa6 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariablePinningTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariablePinningTest.class.php @@ -17,6 +17,7 @@ private function fixtures() { yield ['$y= 2; return [1, 2] is [1, ^$y]', true]; yield ['$y= 2; return [1, 0] is [1, ^$y]', false]; + yield ['return PHP_INT_MAX is ^PHP_INT_MAX', true]; yield ['return [1, 0] is [1, ^self::ZERO]', true]; yield ['return [1, 2] is [1, ^self::ZERO]', false]; } From bb4079bdeacc6bcb93c4b912ddce4ab46683fd7e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 20:14:00 +0100 Subject: [PATCH 12/25] Check with array_key_exists() instead of null-coalescing --- src/main/php/lang/ast/syntax/php/IsOperator.class.php | 9 +++++---- .../ast/syntax/php/unittest/ArrayStructureTest.class.php | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index e99a15f..cf9165b 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -224,10 +224,11 @@ public function setup($language, $emitter) { ) ); foreach ($pattern->patterns as $key => $p) { - $compound= new BinaryExpression($compound, '&&', $match( - $codegen, - new Braced(new BinaryExpression(new OffsetExpression($temp, new Literal((string)$key)), '??', $null)), - $p + $offset= new Literal((string)$key); + $compound= new BinaryExpression($compound, '&&', new BinaryExpression( + new InvokeExpression(new Literal('array_key_exists'), [$offset, $temp]), + '&&', + $match($codegen, new OffsetExpression($temp, $offset), $p), )); } return $compound; diff --git a/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php index 58e82c1..32c1603 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/ArrayStructureTest.class.php @@ -38,6 +38,7 @@ private function fixtures() { yield ['["one" => 1] is ["one" => 1, ...]', true]; yield ['["one" => 1, "two" => 2] is ["one" => 1, ...]', true]; yield ['["two" => 2] is ["one" => 1, ...]', false]; + yield ['["two" => 2] is ["one" => null]', false]; yield ['[2] is [0 => 2]', true]; yield ['[2] is ["0" => 2]', true]; From 453f7c9864045a1c5635bb876b20b2046006e2a6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 20:16:45 +0100 Subject: [PATCH 13/25] Fix expected "Undefined variable" error (differs in PHP 8) --- .../lang/ast/syntax/php/unittest/VariableBindingTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index 874cb91..e0a4e92 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -41,7 +41,7 @@ public function run() { }')); } - #[Test, Expect(class: NullPointerException::class, message: 'Undefined variable: z')] + #[Test, Expect(class: NullPointerException::class, message: '/Undefined variable(.+)z/')] public function delayed_binding() { Assert::equals([false, null], $this->run('use lang\\ast\\syntax\\php\\unittest\\Point; class %T { public function run() { From eff2a7ef8d00db8a6ebe394ed5f5eb2793840e1a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 20:31:05 +0100 Subject: [PATCH 14/25] Add closure See https://externals.io/message/129490#129513 --- .../php/unittest/IsOperatorTest.class.php | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/test/php/lang/ast/syntax/php/unittest/IsOperatorTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/IsOperatorTest.class.php index 6e2bdd9..ddc6e74 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/IsOperatorTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/IsOperatorTest.class.php @@ -7,52 +7,54 @@ class IsOperatorTest extends EmittingTest { #[Test] public function is_mixed_type() { - $r= $this->run('class %T { + Assert::true($this->run('class %T { public function run() { return $this is mixed; } - }'); - - Assert::true($r); + }')); } #[Test] public function precedence() { - $r= $this->run('class %T { + Assert::equals('string ', $this->run('class %T { public function run() { $arg= "Test"; return $arg is string ? sprintf("string <%s>", $arg) : typeof($arg)->literal(); } - }'); - - Assert::equals('string ', $r); + }')); } #[Test, Values([[1, 'int'], ['test', 'string']])] public function with_match_statement($arg, $expected) { - $r= $this->run('class %T { + Assert::equals($expected, $this->run('class %T { public function run(string|int $arg) { return match { $arg is string => "string", $arg is int => "int", }; } - }', $arg); - - Assert::equals($expected, $r); + }', $arg)); } #[Test, Values([[1, 'int'], ['test', 'string']])] public function match_is_variant($arg, $expected) { - $r= $this->run('class %T { + Assert::equals($expected, $this->run('class %T { public function run(string|int $arg) { return match ($arg) is { string => "string", int => "int", }; } - }', $arg); + }', $arg)); + } - Assert::equals($expected, $r); + #[Test, Values([[1, true], ["one", true], [null, false]])] + public function as_closure($arg, $expected) { + Assert::equals($expected, $this->run('class %T { + public function run($arg) { + $f= fn($arg) => $arg is int|string; + return $f($arg); + } + }', $arg)); } } \ No newline at end of file From ab0fe52831bbad1ea433a54d57c8389acda7952a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 21:05:07 +0100 Subject: [PATCH 15/25] Fix binding to variables containing arrays --- .../php/lang/ast/syntax/php/IsOperator.class.php | 12 +++++++----- .../php/unittest/VariableBindingTest.class.php | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index cf9165b..af55634 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -13,6 +13,7 @@ MatchExpression, OffsetExpression, ScopeExpression, + TernaryExpression, Variable }; use lang\ast\syntax\Extension; @@ -196,11 +197,12 @@ public function setup($language, $emitter) { $match($codegen, $temp, $pattern->element) ); } else if ($pattern instanceof IsBinding) { - return new BinaryExpression( - new Literal('true'), - '|', - new Braced(new Assignment($pattern->variable, '=', $expression)) - ); + $true= new Literal('true'); + return new Braced(new TernaryExpression( + new Braced(new Assignment($pattern->variable, '=', $expression)), + $true, + $true + )); } else if ($pattern instanceof IsCompound) { $s= sizeof($pattern->patterns); if (1 === $s) return $match($codegen, $expression, $pattern->patterns[0]); diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index e0a4e92..57645d0 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -28,6 +28,8 @@ private function fixtures() { yield ['[1, 2, 3] is [$x, $y, $z] ? [$x, $y, $z] : null', [1, 2, 3]]; yield ['[0, 1, 2] is [$x, $y, $z] ? [$x, $y, $z] : null', [0, 1, 2]]; + yield ['[1, [1]] is [1, $rest] ? $rest : null', [1]]; + yield ['["one" => 1, "two" => 2] is ["one" => 1, "two" => $t] ? $t : null', 2]; yield ['["one" => 0, "two" => 2] is ["one" => 1, "two" => $t] ? $t : null', null]; } From e96c78a104db6f8b8db4e9ef5b411a5abbad582f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 21:19:20 +0100 Subject: [PATCH 16/25] Add examples inspired by Python's PEP 636 See https://peps.python.org/pep-0636 --- .../unittest/VariableBindingTest.class.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index 57645d0..7043224 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -43,6 +43,30 @@ public function run() { }')); } + #[Test, Values(['get apple', 'pick up apple', 'pick apple up'])] + public function compound_binding($input) { + Assert::equals('apple', $this->run('class %T { + public function run($command) { + return match ($command) is { + ["get", $object]|["pick", "up", $object]|["pick", $object, "up"] => $object, + default => null, + }; + } + }', explode(' ', $input))); + } + + #[Test, Values([['go north', 'north'], ['go south', 'south'], ['go home', null]])] + public function bind_with_subpattern($input, $expected) { + Assert::equals($expected, $this->run('class %T { + public function run($command) { + return match ($command) is { + ["go", $direction & ("north"|"south"|"east"|"west")] => $direction, + default => null, + }; + } + }', explode(' ', $input))); + } + #[Test, Expect(class: NullPointerException::class, message: '/Undefined variable(.+)z/')] public function delayed_binding() { Assert::equals([false, null], $this->run('use lang\\ast\\syntax\\php\\unittest\\Point; class %T { From 020d918d47893cc746535ca99f84c2fe0529e5ea Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Dec 2025 21:27:41 +0100 Subject: [PATCH 17/25] Test "mixed" placeholder inside array --- .../lang/ast/syntax/php/unittest/VariableBindingTest.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index 7043224..0ae9a27 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -29,6 +29,7 @@ private function fixtures() { yield ['[0, 1, 2] is [$x, $y, $z] ? [$x, $y, $z] : null', [0, 1, 2]]; yield ['[1, [1]] is [1, $rest] ? $rest : null', [1]]; + yield ['[1, [2], "three"] is [1, mixed, $rest] ? $rest : null', 'three']; yield ['["one" => 1, "two" => 2] is ["one" => 1, "two" => $t] ? $t : null', 2]; yield ['["one" => 0, "two" => 2] is ["one" => 1, "two" => $t] ? $t : null', null]; From 8d27595b3ff4c94fbfe5138d37f8906f007ea231 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Dec 2025 08:40:15 +0100 Subject: [PATCH 18/25] Use IsIdentical instead of IsComparison The latter is only for numerical comparison --- src/main/php/lang/ast/syntax/php/IsOperator.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index af55634..35836ae 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -54,7 +54,7 @@ public function setup($language, $emitter) { $parse->expecting(')', 'object structure'); } else if ('::' === $parse->token->value) { $parse->forward(); - $r= new IsComparison(new ScopeExpression($r->literal(), new Literal($parse->token->value)), '==='); + $r= new IsIdentical(new ScopeExpression($r->literal(), new Literal($parse->token->value))); $parse->forward(); } } else if ('string' === $parse->token->kind || 'integer' === $parse->token->kind || 'decimal' === $parse->token->kind) { @@ -90,7 +90,7 @@ public function setup($language, $emitter) { $parse->expecting(']', 'array structure'); } else if ('^' === $parse->token->value) { $parse->forward(); - $r= new IsComparison($types->expression($parse, 0), '==='); + $r= new IsIdentical($types->expression($parse, 0)); } else { $parse->expecting('a type or literal', 'is'); return null; From 9f26ec39ccf55153e7d396a61ba9b5019f6c2674 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Dec 2025 08:40:40 +0100 Subject: [PATCH 19/25] QA: Remove commented out debug code --- src/main/php/lang/ast/syntax/php/IsOperator.class.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index 35836ae..b14fae3 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -164,8 +164,6 @@ public function setup($language, $emitter) { }); $match= function($codegen, $expression, $pattern) use(&$match) { - // \util\cmd\Console::writeLine('[...] is ', $pattern); - if ($pattern instanceof IsLiteral) { $literal= $pattern->literal(); if ('mixed' === $literal) { From 57d5da2c5974a3c1220f81d500df0ccc555705ff Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Dec 2025 08:50:15 +0100 Subject: [PATCH 20/25] Do not assign temporary variables to variable expressions See https://github.com/xp-lang/php-is-operator/pull/2#discussion_r2638104241 --- .../lang/ast/syntax/php/IsOperator.class.php | 65 +++++++++++++------ 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index b14fae3..7a490f1 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -178,21 +178,30 @@ public function setup($language, $emitter) { } else if ($pattern instanceof IsIdentical) { return new BinaryExpression($expression, '===', $pattern->value); } else if ($pattern instanceof IsComparison) { - $temp= new Variable($codegen->symbol()); + if ($expression instanceof Variable) { + $use= $init= $expression; + } else { + $use= new Variable($codegen->symbol()); + $init= new Braced(new Assignment($use, '=', $expression)); + } + return new BinaryExpression( - new InvokeExpression( - new Literal('is_numeric'), - [new Braced(new Assignment($temp, '=', $expression))] - ), + new InvokeExpression(new Literal('is_numeric'), [$init]), '&&', - new BinaryExpression($temp, $pattern->operator, $pattern->value) + new BinaryExpression($use, $pattern->operator, $pattern->value) ); } else if ($pattern instanceof IsNullable) { - $temp= new Variable($codegen->symbol()); + if ($expression instanceof Variable) { + $use= $init= $expression; + } else { + $use= new Variable($codegen->symbol()); + $init= new Braced(new Assignment($use, '=', $expression)); + } + return new BinaryExpression( - new BinaryExpression(new Literal('null'), '===', new Braced(new Assignment($temp, '=', $expression))), + new BinaryExpression(new Literal('null'), '===', $init), '||', - $match($codegen, $temp, $pattern->element) + $match($codegen, $use, $pattern->element) ); } else if ($pattern instanceof IsBinding) { $true= new Literal('true'); @@ -205,20 +214,29 @@ public function setup($language, $emitter) { $s= sizeof($pattern->patterns); if (1 === $s) return $match($codegen, $expression, $pattern->patterns[0]); - $temp= new Variable($codegen->symbol()); - $compound= $match($codegen, new Braced(new Assignment($temp, '=', $expression)), $pattern->patterns[0]); + if ($expression instanceof Variable) { + $use= $init= $expression; + } else { + $use= new Variable($codegen->symbol()); + $init= new Braced(new Assignment($use, '=', $expression)); + } + $compound= $match($codegen, $init, $pattern->patterns[0]); for ($i= 1, $op= $pattern->operator.$pattern->operator; $i < $s; $i++) { - $compound= new BinaryExpression($compound, $op, $match($codegen, $temp, $pattern->patterns[$i])); + $compound= new BinaryExpression($compound, $op, $match($codegen, $use, $pattern->patterns[$i])); } return new Braced($compound); } else if ($pattern instanceof IsArrayStructure) { - $null= new Literal('null'); - $temp= new Variable($codegen->symbol()); + if ($expression instanceof Variable) { + $use= $init= $expression; + } else { + $use= new Variable($codegen->symbol()); + $init= new Assignment($use, '=', $expression); + } $compound= new BinaryExpression( - new InvokeExpression(new Literal('is_array'), [new Assignment($temp, '=', $expression)]), + new InvokeExpression(new Literal('is_array'), [$init]), '&&', new BinaryExpression( - new InvokeExpression(new Literal('sizeof'), [$temp]), + new InvokeExpression(new Literal('sizeof'), [$use]), $pattern->rest ? '>=' : '===', new Literal((string)sizeof($pattern->patterns)) ) @@ -226,19 +244,24 @@ public function setup($language, $emitter) { foreach ($pattern->patterns as $key => $p) { $offset= new Literal((string)$key); $compound= new BinaryExpression($compound, '&&', new BinaryExpression( - new InvokeExpression(new Literal('array_key_exists'), [$offset, $temp]), + new InvokeExpression(new Literal('array_key_exists'), [$offset, $use]), '&&', - $match($codegen, new OffsetExpression($temp, $offset), $p), + $match($codegen, new OffsetExpression($use, $offset), $p), )); } return $compound; } else if ($pattern instanceof IsObjectStructure) { - $temp= new Variable($codegen->symbol()); - $compound= new InstanceOfExpression(new Braced(new Assignment($temp, '=', $expression)), $pattern->type); + if ($expression instanceof Variable) { + $use= $init= $expression; + } else { + $use= new Variable($codegen->symbol()); + $init= new Braced(new Assignment($use, '=', $expression)); + } + $compound= new InstanceOfExpression($init, $pattern->type); foreach ($pattern->patterns as $key => $p) { $compound= new BinaryExpression($compound, '&&', $match( $codegen, - new InstanceExpression($temp, new Literal($key)), + new InstanceExpression($use, new Literal($key)), $p )); } From 34c7aa0a2b675ccd28f5034563d6ddf1d70a9308 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Dec 2025 09:01:43 +0100 Subject: [PATCH 21/25] Refactor code, extracting temporary variable handling --- .../lang/ast/syntax/php/IsOperator.class.php | 91 ++++++++----------- 1 file changed, 36 insertions(+), 55 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index 7a490f1..22bdf04 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -1,5 +1,6 @@ literal(); if ('mixed' === $literal) { @@ -175,63 +178,56 @@ public function setup($language, $emitter) { } } else if ($pattern instanceof IsValue) { return new InstanceOfExpression($expression, $pattern); + } else if ($pattern instanceof IsArray || $pattern instanceof IsMap || $pattern instanceof IsFunction) { + return new InvokeExpression(new Literal('is'), [new Literal('"'.$pattern->name().'"'), $expression]); } else if ($pattern instanceof IsIdentical) { return new BinaryExpression($expression, '===', $pattern->value); - } else if ($pattern instanceof IsComparison) { - if ($expression instanceof Variable) { - $use= $init= $expression; - } else { - $use= new Variable($codegen->symbol()); - $init= new Braced(new Assignment($use, '=', $expression)); - } + } else if ($pattern instanceof IsBinding) { + $true= new Literal('true'); + return new Braced(new TernaryExpression( + new Braced(new Assignment($pattern->variable, '=', $expression)), + $true, + $true + )); + } + + // Ensure expressions are only evaluated once. + if ($expression instanceof Variable) { + $use= $init= $expression; + } else { + $use= new Variable($codegen->symbol()); + $init= new Braced(new Assignment($use, '=', $expression)); + } + if ($pattern instanceof IsComparison) { return new BinaryExpression( new InvokeExpression(new Literal('is_numeric'), [$init]), '&&', new BinaryExpression($use, $pattern->operator, $pattern->value) ); } else if ($pattern instanceof IsNullable) { - if ($expression instanceof Variable) { - $use= $init= $expression; - } else { - $use= new Variable($codegen->symbol()); - $init= new Braced(new Assignment($use, '=', $expression)); - } - return new BinaryExpression( new BinaryExpression(new Literal('null'), '===', $init), '||', $match($codegen, $use, $pattern->element) ); - } else if ($pattern instanceof IsBinding) { - $true= new Literal('true'); - return new Braced(new TernaryExpression( - new Braced(new Assignment($pattern->variable, '=', $expression)), - $true, - $true - )); } else if ($pattern instanceof IsCompound) { - $s= sizeof($pattern->patterns); - if (1 === $s) return $match($codegen, $expression, $pattern->patterns[0]); - - if ($expression instanceof Variable) { - $use= $init= $expression; - } else { - $use= new Variable($codegen->symbol()); - $init= new Braced(new Assignment($use, '=', $expression)); - } $compound= $match($codegen, $init, $pattern->patterns[0]); - for ($i= 1, $op= $pattern->operator.$pattern->operator; $i < $s; $i++) { + for ($i= 1, $s= sizeof($pattern->patterns), $op= $pattern->operator.$pattern->operator; $i < $s; $i++) { $compound= new BinaryExpression($compound, $op, $match($codegen, $use, $pattern->patterns[$i])); } return new Braced($compound); - } else if ($pattern instanceof IsArrayStructure) { - if ($expression instanceof Variable) { - $use= $init= $expression; - } else { - $use= new Variable($codegen->symbol()); - $init= new Assignment($use, '=', $expression); + } else if ($pattern instanceof IsObjectStructure) { + $compound= new InstanceOfExpression($init, $pattern->type); + foreach ($pattern->patterns as $key => $p) { + $compound= new BinaryExpression($compound, '&&', $match( + $codegen, + new InstanceExpression($use, new Literal($key)), + $p + )); } + return $compound; + } else if ($pattern instanceof IsArrayStructure) { $compound= new BinaryExpression( new InvokeExpression(new Literal('is_array'), [$init]), '&&', @@ -250,25 +246,10 @@ public function setup($language, $emitter) { )); } return $compound; - } else if ($pattern instanceof IsObjectStructure) { - if ($expression instanceof Variable) { - $use= $init= $expression; - } else { - $use= new Variable($codegen->symbol()); - $init= new Braced(new Assignment($use, '=', $expression)); - } - $compound= new InstanceOfExpression($init, $pattern->type); - foreach ($pattern->patterns as $key => $p) { - $compound= new BinaryExpression($compound, '&&', $match( - $codegen, - new InstanceExpression($use, new Literal($key)), - $p - )); - } - return $compound; - } else { - return new InvokeExpression(new Literal('is'), [new Literal('"'.$pattern->name().'"'), $expression]); } + + // Should be unreachable + throw new IllegalStateException('Unsupported pattern '.$pattern->toString()); }; $emitter->transform('is', function($codegen, $node) use($match) { From d770ec843fdab116de275cb66f5c69f4201ffad7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 23 Dec 2025 08:27:50 +0100 Subject: [PATCH 22/25] Simplify emitted expression for variable binding --- src/main/php/lang/ast/syntax/php/IsOperator.class.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index 22bdf04..f57866a 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -14,7 +14,6 @@ MatchExpression, OffsetExpression, ScopeExpression, - TernaryExpression, Variable }; use lang\ast\syntax\Extension; @@ -183,11 +182,10 @@ public function setup($language, $emitter) { } else if ($pattern instanceof IsIdentical) { return new BinaryExpression($expression, '===', $pattern->value); } else if ($pattern instanceof IsBinding) { - $true= new Literal('true'); - return new Braced(new TernaryExpression( + return new Braced(new BinaryExpression( new Braced(new Assignment($pattern->variable, '=', $expression)), - $true, - $true + '||', + new Literal('true') )); } From fb152b01c60397fde05cbc6975f53256f3525e95 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 23 Dec 2025 08:39:45 +0100 Subject: [PATCH 23/25] Test nested destructuring (`... is [[$a, $b], $c]`) --- .../syntax/php/unittest/VariableBindingTest.class.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index 0ae9a27..ef6984e 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -77,4 +77,13 @@ public function run() { } }')); } + + #[Test] + public function nested_destructuring() { + Assert::equals([1, 2, 3], $this->run('class %T { + public function run() { + return [[1, 2], 3] is [[$a, $b], $c] ? [$a, $b, $c] : null; + } + }')); + } } \ No newline at end of file From 398315effe5802a6f5a6cc314ae921560612c015 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 23 Dec 2025 08:52:16 +0100 Subject: [PATCH 24/25] Add test for nested map destructuring --- .../syntax/php/unittest/VariableBindingTest.class.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index ef6984e..081ae22 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -79,11 +79,20 @@ public function run() { } #[Test] - public function nested_destructuring() { + public function nested_list_destructuring() { Assert::equals([1, 2, 3], $this->run('class %T { public function run() { return [[1, 2], 3] is [[$a, $b], $c] ? [$a, $b, $c] : null; } }')); } + + #[Test] + public function nested_map_destructuring() { + Assert::equals('^8.0', $this->run('class %T { + public function run() { + return ["require" => ["php" => "^8.0"]] is ["require" => ["php" => $version]] ? $version : null; + } + }')); + } } \ No newline at end of file From 372058b99425ea2a0a2fff105349e01d0f0e8a27 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 24 Dec 2025 12:12:20 +0100 Subject: [PATCH 25/25] Make ORing a variable binding a syntax error --- .../lang/ast/syntax/php/IsBinding.class.php | 4 ++- .../lang/ast/syntax/php/IsOperator.class.php | 30 ++++++++++++++----- .../unittest/VariableBindingTest.class.php | 20 +++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/IsBinding.class.php b/src/main/php/lang/ast/syntax/php/IsBinding.class.php index 2d67d7f..3524c03 100755 --- a/src/main/php/lang/ast/syntax/php/IsBinding.class.php +++ b/src/main/php/lang/ast/syntax/php/IsBinding.class.php @@ -4,6 +4,7 @@ class IsBinding extends Type { public $variable; + public $restriction= null; /** * Creates a binding "type" @@ -16,6 +17,7 @@ public function __construct($variable) { /** @return string */ public function toString() { - return nameof($this).'('.$this->variable->pointer.')'; + $restriction= $this->restriction ? ' & '.Objects::stringOf($this->restriction) : ''; + return nameof($this).'('.$this->variable->pointer.$restriction.')'; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php index f57866a..7e34fc1 100755 --- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php +++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php @@ -64,6 +64,13 @@ public function setup($language, $emitter) { $parse->forward(); $r= new IsBinding(new Variable($parse->token->value)); $parse->forward(); + + // See https://wiki.php.net/rfc/pattern-matching#applying_patterns_to_bound_variables + if ('&' === $parse->token->value) { + $parse->forward(); + $r->restriction= $pattern($parse, $types); + } + return $r; } else if ('>' === $parse->token->value || '>=' === $parse->token->value || '<' === $parse->token->value || '<=' === $parse->token->value) { $operator= $parse->token->value; $parse->forward(); @@ -108,9 +115,9 @@ public function setup($language, $emitter) { } else { return new IsCompound([$r, $n], $operator); } - } else { - return $r; } + + return $r; }; $language->infix('is', 60, function($parse, $token, $left) use($pattern) { @@ -182,11 +189,20 @@ public function setup($language, $emitter) { } else if ($pattern instanceof IsIdentical) { return new BinaryExpression($expression, '===', $pattern->value); } else if ($pattern instanceof IsBinding) { - return new Braced(new BinaryExpression( - new Braced(new Assignment($pattern->variable, '=', $expression)), - '||', - new Literal('true') - )); + $bind= new Assignment($pattern->variable, '=', $expression); + $compound= new Braced(new BinaryExpression(new Braced($bind), '||', new Literal('true'))); + + // Assign to temporary variable, only actually bind if restriction matches + if ($pattern->restriction) { + $bind->expression= new Variable($codegen->symbol()); + $compound= new BinaryExpression( + $match($codegen, new Braced(new Assignment($bind->expression, '=', $expression)), $pattern->restriction), + '&&', + $compound + ); + } + + return $compound; } // Ensure expressions are only evaluated once. diff --git a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php index 081ae22..3c02f5e 100755 --- a/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php +++ b/src/test/php/lang/ast/syntax/php/unittest/VariableBindingTest.class.php @@ -1,6 +1,7 @@ run('class %T { + public function run() { + [] is [$variable | 0]; + } + }'); + } + #[Test, Expect(class: NullPointerException::class, message: '/Undefined variable(.+)z/')] public function delayed_binding() { Assert::equals([false, null], $this->run('use lang\\ast\\syntax\\php\\unittest\\Point; class %T { @@ -95,4 +105,14 @@ public function run() { } }')); } + + #[Test, Values(['[] is [$var]', '["test"] is [$var & int]'])] + public function variable_unset_when_unmatched($expr) { + Assert::false($this->run('class %T { + public function run() { + '.$expr.'; + return isset($var); + } + }')); + } } \ No newline at end of file