From b29dbc7aeaa82ff8cc7e7599f83b86670f5b7a3d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 13:44:08 +0200 Subject: [PATCH 01/17] Add support for generic methods --- src/main/php/lang/GenericTypes.class.php | 28 ++++++++++++++++--- .../php/lang/unittest/GenericsTest.class.php | 9 ++++++ src/test/php/lang/unittest/ListOf.class.php | 13 +++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index f4b52b855..78b4c5cbe 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -185,6 +185,13 @@ public function newType0($base, $arguments) { $m= $tokens[$i+ 2][1]; $p= 0; $generic= $reflect->getMethod($m)->getAttributes(Generic::class); + $annotations= $generic ? $generic[0]->getArguments() : []; + $typeargs= []; + if (isset($annotations['self'])) { + foreach (Type::split($annotations['self']) as $p => $typearg) { + $typeargs[ltrim($typearg)]= $p; + } + } } else if (T_VARIABLE === $tokens[$i][0]) { $f= substr($tokens[$i][1], 1); $generic= $reflect->getProperty($f)->getAttributes(Generic::class); @@ -193,6 +200,12 @@ public function newType0($base, $arguments) { $meta[0][$f][DETAIL_RETURNS]= strtr($annotations['var'], $placeholders); } } else if ('}' === $tokens[$i][0]) { + $reflect->isInterface() || $src.= ( + 'function __call($name, $arguments) {'. + ' $p= strpos($name, "<") ?: throw new \Error("Call to undefined method", $name);'. + ' return $this->{substr($name, 0, $p)}(\lang\Type::forNames(substr($name, $p + 1, -1)), ...$arguments);'. + '}' + ); $src.= '}'; break; } else if (T_CLOSE_TAG === $tokens[$i][0]) { @@ -201,6 +214,10 @@ public function newType0($base, $arguments) { } else if (2 === $state[0]) { // Method declaration if ('(' === $tokens[$i][0]) { $braces++; + if (1 === $braces && $typeargs) { + $src.= '($__T,'; + continue; + } } else if (')' === $tokens[$i][0]) { $braces--; if (0 === $braces) { @@ -238,7 +255,6 @@ public function newType0($base, $arguments) { array_shift($state); array_unshift($state, 4); $src.= '{'; - $annotations= $generic ? $generic[0]->getArguments() : []; if (isset($annotations['return'])) { $meta[1][$m][DETAIL_RETURNS]= strtr($annotations['return'], $placeholders); } @@ -278,8 +294,12 @@ public function newType0($base, $arguments) { } else if ('}' === $tokens[$i][0]) { $braces--; if (0 === $braces) array_shift($state); - } else if (T_VARIABLE === $tokens[$i][0] && isset($placeholders[$v= substr($tokens[$i][1], 1)])) { - $src.= 'self::$__generic["'.$v.'"]'; + } else if (T_VARIABLE === $tokens[$i][0]) { + $v= substr($tokens[$i][1], 1); + $src.= isset($placeholders[$v]) + ? 'self::$__generic["'.$v.'"]' : + (isset($typeargs[$v]) ? '$__T['.$typeargs[$v].']' : $tokens[$i][1]) + ; continue; } } else if (5 === $state[0]) { // Implements (class), Extends (interface) @@ -322,7 +342,7 @@ public function newType0($base, $arguments) { } // Create class - // fputs(STDERR, "@* ".substr($src, 0, strpos($src, '{'))." -> $qname\n"); + // var_dump([$qname => $src]); eval($src); if ($initialize) { foreach ($components as $i => $component) { diff --git a/src/test/php/lang/unittest/GenericsTest.class.php b/src/test/php/lang/unittest/GenericsTest.class.php index fb6987ef3..01857571a 100755 --- a/src/test/php/lang/unittest/GenericsTest.class.php +++ b/src/test/php/lang/unittest/GenericsTest.class.php @@ -93,4 +93,13 @@ public function pass_invalid($arguments) { public function pass_invalid_varargs($arguments) { create('new lang.unittest.ListOf', ...$arguments); } + + #[Test] + public function invoke_generic_method() { + $fixture= create('new lang.unittest.ListOf', 'Hello', 'World!'); + $mapped= $fixture->{'map'}('strlen'); + + Assert::instance('lang.unittest.ListOf', $mapped); + Assert::equals([5, 6], $mapped->elements); + } } \ No newline at end of file diff --git a/src/test/php/lang/unittest/ListOf.class.php b/src/test/php/lang/unittest/ListOf.class.php index f17642c36..1ad99d89b 100755 --- a/src/test/php/lang/unittest/ListOf.class.php +++ b/src/test/php/lang/unittest/ListOf.class.php @@ -28,6 +28,19 @@ public function extend($args) { return $this; } + /** + * Applies a given map function to all elements in this list, + * returning a new list with the mapped elements. + */ + #[Generic(self: 'M')] + public function map($map) { + $m= create("new lang.unittest.ListOf<$M>"); + foreach ($this->elements as $element) { + $m->elements[]= $map($element); + } + return $m; + } + /** * Returns a list of all elements * From d4c1e62d56a0754aa3911771d7ab93150b6cb6ea Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 14:54:15 +0200 Subject: [PATCH 02/17] Implement argument type verification for generic methods --- src/main/php/lang/GenericTypes.class.php | 8 ++++++-- src/test/php/lang/unittest/GenericsTest.class.php | 5 +++++ src/test/php/lang/unittest/ListOf.class.php | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 78b4c5cbe..257edd3b3 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -260,8 +260,12 @@ public function newType0($base, $arguments) { } if (isset($annotations['params'])) { $generic= []; + $replace= $placeholders; + foreach ($typeargs as $placeholder => $p) { + $replace[$placeholder]= "\$__T[{$p}]"; + } foreach (Type::split($annotations['params']) as $j => $placeholder) { - if ('' === ($replaced= strtr($placeholder, $placeholders))) { + if ('' === ($replaced= strtr($placeholder, $replace))) { $generic[$j]= null; } else { $meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced; @@ -279,7 +283,7 @@ public function newType0($base, $arguments) { $src.= ( ' if ('.(isset($default[$j]) ? '('.$default[$j].' !== '.$parameters[$j].') && ' : ''). - '!instance(\''.$verify.'\', '.$parameters[$j].')) throw new \lang\IllegalArgumentException('. + '!instance("'.$verify.'", '.$parameters[$j].')) throw new \lang\IllegalArgumentException('. '"Argument '.($j + 1).' passed to ".__METHOD__."'. ' must be of '.$type.', ".typeof('.$parameters[$j].')." given"'. ');' diff --git a/src/test/php/lang/unittest/GenericsTest.class.php b/src/test/php/lang/unittest/GenericsTest.class.php index 01857571a..c0ef353b3 100755 --- a/src/test/php/lang/unittest/GenericsTest.class.php +++ b/src/test/php/lang/unittest/GenericsTest.class.php @@ -102,4 +102,9 @@ public function invoke_generic_method() { Assert::instance('lang.unittest.ListOf', $mapped); Assert::equals([5, 6], $mapped->elements); } + + #[Test, Expect(IllegalArgumentException::class)] + public function generic_method_invalid_argument() { + create('new lang.unittest.ListOf')->{'map'}(null); + } } \ No newline at end of file diff --git a/src/test/php/lang/unittest/ListOf.class.php b/src/test/php/lang/unittest/ListOf.class.php index 1ad99d89b..52ec9e93a 100755 --- a/src/test/php/lang/unittest/ListOf.class.php +++ b/src/test/php/lang/unittest/ListOf.class.php @@ -32,7 +32,7 @@ public function extend($args) { * Applies a given map function to all elements in this list, * returning a new list with the mapped elements. */ - #[Generic(self: 'M')] + #[Generic(self: 'M', params: 'function(T): M')] public function map($map) { $m= create("new lang.unittest.ListOf<$M>"); foreach ($this->elements as $element) { From 7d2b5e7a0751fcdfcb6e1994710129110b239fba Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 14:55:25 +0200 Subject: [PATCH 03/17] QA: Add import for `ReflectionClass` --- src/main/php/lang/GenericTypes.class.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 257edd3b3..bcd9f5f34 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -1,5 +1,7 @@ newType0($base, $arguments))); + return new XPClass(new ReflectionClass($this->newType0($base, $arguments))); } /** @@ -334,7 +336,7 @@ public function newType0($base, $arguments) { foreach (Type::split($annotation[$counter]) as $j => $placeholder) { $iargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders)); } - $src.= '\\'.$this->newType0(new XPClass(new \ReflectionClass($rel)), $iargs); + $src.= '\\'.$this->newType0(new XPClass(new ReflectionClass($rel)), $iargs); } else { $src.= $rel; } From bf94a97562bd4086d2c604d0c3309addd3e39641 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 15:07:45 +0200 Subject: [PATCH 04/17] Support `new self` inside `create()` --- src/main/php/lang.base.php | 12 +++++++++--- src/test/php/lang/unittest/ListOf.class.php | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang.base.php b/src/main/php/lang.base.php index fef176bd3..61ec45f73 100755 --- a/src/main/php/lang.base.php +++ b/src/main/php/lang.base.php @@ -334,12 +334,18 @@ function create($spec, ... $args) { // TYPE:= B "<" ARGS ">" // ARGS:= TYPE [ "," TYPE [ "," ... ]] $b= strpos($spec, '<'); - $base= substr($spec, 4, $b- 4); - $typeargs= \lang\Type::forNames(substr($spec, $b+ 1, strrpos($spec, '>')- $b- 1)); + $base= substr($spec, 4, $b - 4); + + if ('self' === $base) { + $context= debug_backtrace(0, 2)[1]['class']; + $class= new \lang\XPClass(substr($context, 0, strcspn($context, "\xb7"))); + } else { + $class= \lang\XPClass::forName($base); + } // Instantiate, passing the rest of any arguments passed to create() // BC: Wrap IllegalStateExceptions into IllegalArgumentExceptions - $class= \lang\XPClass::forName(strstr($base, '.') ? $base : \lang\XPClass::nameOf($base)); + $typeargs= \lang\Type::forNames(substr($spec, $b+ 1, strrpos($spec, '>')- $b- 1)); try { return $class->newGenericType($typeargs)->newInstance(...$args); } catch (\lang\IllegalStateException $e) { diff --git a/src/test/php/lang/unittest/ListOf.class.php b/src/test/php/lang/unittest/ListOf.class.php index 52ec9e93a..8dcca72c5 100755 --- a/src/test/php/lang/unittest/ListOf.class.php +++ b/src/test/php/lang/unittest/ListOf.class.php @@ -32,9 +32,9 @@ public function extend($args) { * Applies a given map function to all elements in this list, * returning a new list with the mapped elements. */ - #[Generic(self: 'M', params: 'function(T): M')] + #[Generic(self: 'M', params: 'function(T): M', return: 'self')] public function map($map) { - $m= create("new lang.unittest.ListOf<$M>"); + $m= create("new self<$M>"); foreach ($this->elements as $element) { $m->elements[]= $map($element); } From bf2affe52e344e908e4fc1211ae1c113f8a2ad2e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 15:59:09 +0200 Subject: [PATCH 05/17] Remove IllegalStateExceptions -> IllegalArgumentExceptions wrapping in create() --- src/main/php/lang.base.php | 3 --- src/test/php/lang/unittest/CreateTest.class.php | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang.base.php b/src/main/php/lang.base.php index 61ec45f73..22feb99e6 100755 --- a/src/main/php/lang.base.php +++ b/src/main/php/lang.base.php @@ -344,12 +344,9 @@ function create($spec, ... $args) { } // Instantiate, passing the rest of any arguments passed to create() - // BC: Wrap IllegalStateExceptions into IllegalArgumentExceptions $typeargs= \lang\Type::forNames(substr($spec, $b+ 1, strrpos($spec, '>')- $b- 1)); try { return $class->newGenericType($typeargs)->newInstance(...$args); - } catch (\lang\IllegalStateException $e) { - throw new \lang\IllegalArgumentException($e->getMessage()); } catch (ReflectionException $e) { throw new \lang\IllegalAccessException($e->getMessage()); } diff --git a/src/test/php/lang/unittest/CreateTest.class.php b/src/test/php/lang/unittest/CreateTest.class.php index ef2ff855f..302acddfb 100755 --- a/src/test/php/lang/unittest/CreateTest.class.php +++ b/src/test/php/lang/unittest/CreateTest.class.php @@ -1,6 +1,6 @@ '); } From f915dca796917d8df0d31a02b658d6d8cbac4acf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 18:40:39 +0200 Subject: [PATCH 06/17] Add tests for generic methods with components --- src/main/php/lang/GenericTypes.class.php | 15 ++++++++++++++- .../php/lang/unittest/GenericsTest.class.php | 11 +++++++++++ src/test/php/lang/unittest/Lookup.class.php | 17 +++++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index bcd9f5f34..8dd1ffe41 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -233,7 +233,7 @@ public function newType0($base, $arguments) { } else if (',' === $tokens[$i][0]) { // Skip } else if ('=' === $tokens[$i][0]) { - $p= sizeof($parameters)- 1; + $p= sizeof($parameters) - 1; $default[$p]= ''; } else if (T_WHITESPACE !== $tokens[$i][0] && isset($default[$p])) { $default[$p].= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; @@ -300,6 +300,8 @@ public function newType0($base, $arguments) { } else if ('}' === $tokens[$i][0]) { $braces--; if (0 === $braces) array_shift($state); + } else if ('"' === $tokens[$i][0]) { + array_unshift($state, T_STRING_VARNAME); } else if (T_VARIABLE === $tokens[$i][0]) { $v= substr($tokens[$i][1], 1); $src.= isset($placeholders[$v]) @@ -308,6 +310,17 @@ public function newType0($base, $arguments) { ; continue; } + } else if (T_STRING_VARNAME === $state[0]) { + if ('"' === $tokens[$i][0]) { + array_shift($state); + } else if (T_VARIABLE === $tokens[$i][0]) { + $v= substr($tokens[$i][1], 1); + $src.= isset($placeholders[$v]) + ? '".self::$__generic["'.$v.'"]."' : + (isset($typeargs[$v]) ? '$__T['.$typeargs[$v].']' : $tokens[$i][1]) + ; + continue; + } } else if (5 === $state[0]) { // Implements (class), Extends (interface) if ('{' === $tokens[$i]) { array_shift($state); diff --git a/src/test/php/lang/unittest/GenericsTest.class.php b/src/test/php/lang/unittest/GenericsTest.class.php index c0ef353b3..d532b55e3 100755 --- a/src/test/php/lang/unittest/GenericsTest.class.php +++ b/src/test/php/lang/unittest/GenericsTest.class.php @@ -103,6 +103,17 @@ public function invoke_generic_method() { Assert::equals([5, 6], $mapped->elements); } + #[Test] + public function generic_method_components() { + $fixture= create('new lang.unittest.Lookup'); + $fixture->put('greeting', 'Hello'); + $fixture->put('person', 'Tester'); + $mapped= $fixture->{'map'}('strlen'); + + Assert::instance('lang.unittest.Lookup', $mapped); + Assert::equals([5, 6], $mapped->values()); + } + #[Test, Expect(IllegalArgumentException::class)] public function generic_method_invalid_argument() { create('new lang.unittest.ListOf')->{'map'}(null); diff --git a/src/test/php/lang/unittest/Lookup.class.php b/src/test/php/lang/unittest/Lookup.class.php index 35627fdd4..076481215 100755 --- a/src/test/php/lang/unittest/Lookup.class.php +++ b/src/test/php/lang/unittest/Lookup.class.php @@ -5,10 +5,10 @@ #[Generic(self: 'K, V', parent: 'K, V')] class Lookup extends AbstractDictionary { - protected $size= 0; + private $size= 0; #[Generic(['var' => '[:V]'])] - protected $elements= []; + public $elements= []; /** * Put a key/value pairt @@ -38,6 +38,19 @@ public function get($key) { return $this->elements[$offset]; } + /** + * Applies a given map function to all elements in this lookup, + * returning a new list with the mapped elements. + */ + #[Generic(self: 'M', params: 'function(V): M', return: 'self')] + public function map($map) { + $m= create("new self<$K, $M>"); + foreach ($this->elements as $hash => $element) { + $m->elements[$hash]= $map($element); + } + return $m; + } + /** * Returns all values * From 1408a36b092d8941f34b6a4b77eaac0f1f085d4c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 18:52:02 +0200 Subject: [PATCH 07/17] Use constants for state * 1 => `T_CLASS_C` * 2 => `T_METHOD_C` * 3 => `T_CALLABLE` * 4 => `T_FUNCTION` * 5 => `T_IMPLEMENTS` --- src/main/php/lang/GenericTypes.class.php | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 8dd1ffe41..5b2f26614 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -157,33 +157,33 @@ public function newType0($base, $arguments) { $counter= 0; $annotation= $annotated['implements'] ?? null; array_unshift($state, T_CLASS); - array_unshift($state, 5); + array_unshift($state, T_IMPLEMENTS); } else if ('{' === $tokens[$i][0]) { array_shift($state); - array_unshift($state, 1); + array_unshift($state, T_CLASS_C); $src.= ' { public static $__generic= [];'; $initialize= true; } continue; } else if (T_INTERFACE === $state[0]) { - if (T_EXTENDS === $tokens[$i][0]) { + if (T_IMPLEMENTS === $tokens[$i][0]) { $src.= ' extends'; $counter= 0; $annotation= $annotated['extends'] ?? null; array_unshift($state, T_INTERFACE); - array_unshift($state, 5); + array_unshift($state, T_IMPLEMENTS); } else if ('{' === $tokens[$i][0]) { array_shift($state); - array_unshift($state, 1); + array_unshift($state, T_CLASS_C); $src.= ' {'; } continue; - } else if (1 === $state[0]) { // Class body + } else if (T_CLASS_C === $state[0]) { if (T_FUNCTION === $tokens[$i][0]) { $braces= 0; $parameters= $default= []; - array_unshift($state, 3); - array_unshift($state, 2); + array_unshift($state, T_CALLABLE); + array_unshift($state, T_METHOD_C); $m= $tokens[$i+ 2][1]; $p= 0; $generic= $reflect->getMethod($m)->getAttributes(Generic::class); @@ -213,7 +213,7 @@ public function newType0($base, $arguments) { } else if (T_CLOSE_TAG === $tokens[$i][0]) { break; } - } else if (2 === $state[0]) { // Method declaration + } else if (T_METHOD_C === $state[0]) { if ('(' === $tokens[$i][0]) { $braces++; if (1 === $braces && $typeargs) { @@ -238,7 +238,7 @@ public function newType0($base, $arguments) { } else if (T_WHITESPACE !== $tokens[$i][0] && isset($default[$p])) { $default[$p].= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; } - } else if (3 === $state[0]) { // Method body + } else if (T_CALLABLE === $state[0]) { if (';' === $tokens[$i][0]) { // Abstract method $annotations= $generic ? $generic[0]->getArguments() : []; if (isset($annotations['return'])) { @@ -255,7 +255,7 @@ public function newType0($base, $arguments) { } else if ('{' === $tokens[$i][0]) { $braces= 1; array_shift($state); - array_unshift($state, 4); + array_unshift($state, T_FUNCTION); $src.= '{'; if (isset($annotations['return'])) { $meta[1][$m][DETAIL_RETURNS]= strtr($annotations['return'], $placeholders); @@ -294,7 +294,7 @@ public function newType0($base, $arguments) { } continue; } - } else if (4 === $state[0]) { // Method body + } else if (T_FUNCTION === $state[0]) { if ('{' === $tokens[$i][0] || T_CURLY_OPEN === $tokens[$i][0]) { $braces++; } else if ('}' === $tokens[$i][0]) { @@ -321,7 +321,7 @@ public function newType0($base, $arguments) { ; continue; } - } else if (5 === $state[0]) { // Implements (class), Extends (interface) + } else if (T_IMPLEMENTS === $state[0]) { // Implements (class), Extends (interface) if ('{' === $tokens[$i]) { array_shift($state); array_shift($state); From c5081ab6c84ca4b67bd8c5d6dc0d5d3299fdb479 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 19:59:02 +0200 Subject: [PATCH 08/17] QA: Use type hints --- src/main/php/lang/GenericTypes.class.php | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 5b2f26614..53fc923e5 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -5,29 +5,18 @@ /** * Generate generic runtime types. * + * @test lang.unittest.generics.GenericsTest * @test lang.unittest.generics.GenericTypesTest */ class GenericTypes { - /** - * Creates a generic type - * - * @param lang.XPClass base - * @param lang.Type[] arguments - * @return lang.XPClass created type - */ - public function newType(XPClass $base, array $arguments) { + /** Creates a generic type */ + public function newType(XPClass $base, array $arguments): XPClass { return new XPClass(new ReflectionClass($this->newType0($base, $arguments))); } - /** - * Creates a generic type - * - * @param lang.XPClass base - * @param lang.Type[] arguments - * @return string created type's literal - */ - public function newType0($base, $arguments) { + /** Creates a generic type and returns the created type's literal */ + public function newType0(XPClass $base, array $arguments): string { $reflect= $base->reflect(); $generic= $reflect->getAttributes(Generic::class); if (empty($generic) || (($annotated= $generic[0]->getArguments()) && !isset($annotated['self']))) { From 7b4b100d493497ba05c0110648418c1fbd5d2b96 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 20:43:06 +0200 Subject: [PATCH 09/17] Fix generic interface with parent interfaces --- src/main/php/lang/GenericTypes.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 53fc923e5..c5c5f468e 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -155,7 +155,7 @@ public function newType0(XPClass $base, array $arguments): string { } continue; } else if (T_INTERFACE === $state[0]) { - if (T_IMPLEMENTS === $tokens[$i][0]) { + if (T_EXTENDS === $tokens[$i][0]) { $src.= ' extends'; $counter= 0; $annotation= $annotated['extends'] ?? null; From 75e4c36be9c11d4fd504830033a83ba106be3541 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 20:47:09 +0200 Subject: [PATCH 10/17] Test nullable types --- .../php/lang/unittest/GenericsTest.class.php | 10 ++++++++++ src/test/php/lang/unittest/ListOf.class.php | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/unittest/GenericsTest.class.php b/src/test/php/lang/unittest/GenericsTest.class.php index d532b55e3..4562f7c73 100755 --- a/src/test/php/lang/unittest/GenericsTest.class.php +++ b/src/test/php/lang/unittest/GenericsTest.class.php @@ -84,6 +84,16 @@ public function pass_array_argument() { Assert::equals(['Hello', 'World', '!'], $fixture->elements); } + #[Test] + public function pass_nullable() { + $fixture= create('new lang.unittest.ListOf') + ->append('Hello') + ->append(null) + ; + + Assert::equals(['Hello', null], $fixture->elements); + } + #[Test, Expect(IllegalArgumentException::class), Values([[[1]], [['Test', 1]], [[null, 'Test']]])] public function pass_invalid($arguments) { create('new lang.unittest.ListOf', 'Hello')->extend($arguments); diff --git a/src/test/php/lang/unittest/ListOf.class.php b/src/test/php/lang/unittest/ListOf.class.php index 8dcca72c5..c05b9a469 100755 --- a/src/test/php/lang/unittest/ListOf.class.php +++ b/src/test/php/lang/unittest/ListOf.class.php @@ -16,6 +16,18 @@ public function __construct(... $args) { $this->elements= $args; } + /** + * Extends this list with all given arguments + * + * @param T element + * @return self + */ + #[Generic(params: 'T')] + public function append($element) { + $this->elements[]= $element; + return $this; + } + /** * Extends this list with all given arguments * @@ -23,8 +35,8 @@ public function __construct(... $args) { * @return self */ #[Generic(params: 'T[]')] - public function extend($args) { - $this->elements= array_merge($this->elements, $args); + public function extend($elements) { + $this->elements= array_merge($this->elements, $elements); return $this; } From ee5cfad423d045e5205b42ef152f9f1862e7d4a4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Aug 2026 20:48:45 +0200 Subject: [PATCH 11/17] Test union types --- src/test/php/lang/unittest/GenericsTest.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/php/lang/unittest/GenericsTest.class.php b/src/test/php/lang/unittest/GenericsTest.class.php index 4562f7c73..27a515689 100755 --- a/src/test/php/lang/unittest/GenericsTest.class.php +++ b/src/test/php/lang/unittest/GenericsTest.class.php @@ -94,6 +94,16 @@ public function pass_nullable() { Assert::equals(['Hello', null], $fixture->elements); } + #[Test] + public function pass_union() { + $fixture= create('new lang.unittest.ListOf') + ->append(1) + ->append(1.5) + ; + + Assert::equals([1, 1.5], $fixture->elements); + } + #[Test, Expect(IllegalArgumentException::class), Values([[[1]], [['Test', 1]], [[null, 'Test']]])] public function pass_invalid($arguments) { create('new lang.unittest.ListOf', 'Hello')->extend($arguments); From f78df7e61c3d448d336f2b9c503c6f02176d5370 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Aug 2026 09:05:38 +0200 Subject: [PATCH 12/17] Raise an error in create() when given type does not have type arguments --- src/main/php/lang.base.php | 16 +++++++--------- src/test/php/lang/unittest/CreateTest.class.php | 5 +++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang.base.php b/src/main/php/lang.base.php index 22feb99e6..134ea76ad 100755 --- a/src/main/php/lang.base.php +++ b/src/main/php/lang.base.php @@ -323,19 +323,17 @@ function newinstance($spec, $args, $def= null) { } // }}} -// {{{ proto object create(string spec, var... $args) +// {{{ proto object create(string type, var... $args) // Creates a generic object -function create($spec, ... $args) { - if (!is_string($spec)) { - throw new \lang\IllegalArgumentException('Create expects its first argument to be a string'); +function create(string $type, ... $args) { + if (false === ($b= strpos($type, '<'))) { + throw new \lang\IllegalArgumentException('Type '.$type.' does not have type arguments'); } - // Parse type specification: "new " TYPE "()"? + // Parse type specification: "new " TYPE "()" // TYPE:= B "<" ARGS ">" // ARGS:= TYPE [ "," TYPE [ "," ... ]] - $b= strpos($spec, '<'); - $base= substr($spec, 4, $b - 4); - + $base= substr($type, 4, $b - 4); if ('self' === $base) { $context= debug_backtrace(0, 2)[1]['class']; $class= new \lang\XPClass(substr($context, 0, strcspn($context, "\xb7"))); @@ -344,7 +342,7 @@ function create($spec, ... $args) { } // Instantiate, passing the rest of any arguments passed to create() - $typeargs= \lang\Type::forNames(substr($spec, $b+ 1, strrpos($spec, '>')- $b- 1)); + $typeargs= \lang\Type::forNames(substr($type, $b + 1, strrpos($type, '>') - $b - 1)); try { return $class->newGenericType($typeargs)->newInstance(...$args); } catch (ReflectionException $e) { diff --git a/src/test/php/lang/unittest/CreateTest.class.php b/src/test/php/lang/unittest/CreateTest.class.php index 302acddfb..b75a836df 100755 --- a/src/test/php/lang/unittest/CreateTest.class.php +++ b/src/test/php/lang/unittest/CreateTest.class.php @@ -28,6 +28,11 @@ public function create_invokes_constructor() { ); } + #[Test, Expect(IllegalArgumentException::class)] + public function create_raises_exception_when_type_args_missing() { + create('new lang.unittest.Lookup'); + } + #[Test, Expect(IllegalStateException::class)] public function create_raises_exception_when_non_generic_given() { create('new lang.unittest.Name'); From 3e8254b2a751c38bd86d46ed25cc1ce6f614d14b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Aug 2026 14:30:32 +0200 Subject: [PATCH 13/17] Fix imported names not being fully qualified --- src/main/php/lang/GenericTypes.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index c5c5f468e..b764fdbfb 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -323,9 +323,9 @@ public function newType0(XPClass $base, array $arguments): string { $i++; } $i--; - '\\' === $rel[0] || $rel= isset($imports[$rel]) ? $imports[$rel] : '\\'.$namespace.'\\'.$rel; + '\\' === $rel[0] || $rel= '\\'.($imports[$rel] ?? $namespace.'\\'.$rel); } else if (T_NAME_QUALIFIED === $tokens[$i][0]) { - $rel= isset($imports[$tokens[$i][1]]) ? $imports[$tokens[$i][1]] : '\\'.$namespace.'\\'.$tokens[$i][1]; + $rel= '\\'.($imports[$tokens[$i][1]] ?? $namespace.'\\'.$tokens[$i][1]); } else if (T_NAME_FULLY_QUALIFIED === $tokens[$i][0]) { $rel= $tokens[$i][1]; } else { From f276ae5b4069a73b8c3de41ee432f3b0e55a1906 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Aug 2026 15:06:17 +0200 Subject: [PATCH 14/17] Fix variadic types in meta information --- src/main/php/lang/GenericTypes.class.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index b764fdbfb..7fb2c9218 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -255,6 +255,7 @@ public function newType0(XPClass $base, array $arguments): string { foreach ($typeargs as $placeholder => $p) { $replace[$placeholder]= "\$__T[{$p}]"; } + $replace['...']= '[]'; foreach (Type::split($annotations['params']) as $j => $placeholder) { if ('' === ($replaced= strtr($placeholder, $replace))) { $generic[$j]= null; @@ -264,17 +265,9 @@ public function newType0(XPClass $base, array $arguments): string { } } foreach ($generic as $j => $type) { - if (null === $type) { - continue; - } else if ('...' === substr($type, -3)) { - $verify= substr($generic[$j], 0, -3).'[]'; - } else { - $verify= $generic[$j]; - } - - $src.= ( + isset($type) && $src.= ( ' if ('.(isset($default[$j]) ? '('.$default[$j].' !== '.$parameters[$j].') && ' : ''). - '!instance("'.$verify.'", '.$parameters[$j].')) throw new \lang\IllegalArgumentException('. + '!instance("'.$type.'", '.$parameters[$j].')) throw new \lang\IllegalArgumentException('. '"Argument '.($j + 1).' passed to ".__METHOD__."'. ' must be of '.$type.', ".typeof('.$parameters[$j].')." given"'. ');' From 9a04e6340d7e72cb2233d1e9449927c45640252e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Aug 2026 18:21:58 +0200 Subject: [PATCH 15/17] Do not replace typeargs in meta data --- src/main/php/lang/GenericTypes.class.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 7fb2c9218..e0a8ead33 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -251,17 +251,16 @@ public function newType0(XPClass $base, array $arguments): string { } if (isset($annotations['params'])) { $generic= []; - $replace= $placeholders; + $replace= $rewrite= ['...' => '[]'] + $placeholders; foreach ($typeargs as $placeholder => $p) { $replace[$placeholder]= "\$__T[{$p}]"; } - $replace['...']= '[]'; foreach (Type::split($annotations['params']) as $j => $placeholder) { - if ('' === ($replaced= strtr($placeholder, $replace))) { - $generic[$j]= null; + if ('' === $placeholder) { + $generic[]= null; } else { - $meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced; - $generic[$j]= $replaced; + $generic[]= strtr($placeholder, $replace); + $meta[1][$m][DETAIL_ARGUMENTS][$j]= strtr($placeholder, $rewrite); } } foreach ($generic as $j => $type) { From 936a628002a2747ac1813a4b7057fe443c31c18e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Aug 2026 18:37:40 +0200 Subject: [PATCH 16/17] Fold loops --- src/main/php/lang/GenericTypes.class.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index e0a8ead33..3b056adf8 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -250,21 +250,16 @@ public function newType0(XPClass $base, array $arguments): string { $meta[1][$m][DETAIL_RETURNS]= strtr($annotations['return'], $placeholders); } if (isset($annotations['params'])) { - $generic= []; $replace= $rewrite= ['...' => '[]'] + $placeholders; foreach ($typeargs as $placeholder => $p) { $replace[$placeholder]= "\$__T[{$p}]"; } foreach (Type::split($annotations['params']) as $j => $placeholder) { - if ('' === $placeholder) { - $generic[]= null; - } else { - $generic[]= strtr($placeholder, $replace); - $meta[1][$m][DETAIL_ARGUMENTS][$j]= strtr($placeholder, $rewrite); - } - } - foreach ($generic as $j => $type) { - isset($type) && $src.= ( + if ('' === $placeholder) continue; + + $meta[1][$m][DETAIL_ARGUMENTS][$j]= strtr($placeholder, $rewrite); + $type= strtr($placeholder, $replace); + $src.= ( ' if ('.(isset($default[$j]) ? '('.$default[$j].' !== '.$parameters[$j].') && ' : ''). '!instance("'.$type.'", '.$parameters[$j].')) throw new \lang\IllegalArgumentException('. '"Argument '.($j + 1).' passed to ".__METHOD__."'. From d9c7503a8f6828096d31029d30f553e013926352 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Aug 2026 14:30:32 +0200 Subject: [PATCH 17/17] Fix imported names not being fully qualified --- src/main/php/lang/GenericTypes.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index f4b52b855..24f8a5761 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -295,9 +295,9 @@ public function newType0($base, $arguments) { $i++; } $i--; - '\\' === $rel[0] || $rel= isset($imports[$rel]) ? $imports[$rel] : '\\'.$namespace.'\\'.$rel; + '\\' === $rel[0] || $rel= '\\'.($imports[$rel] ?? $namespace.'\\'.$rel); } else if (T_NAME_QUALIFIED === $tokens[$i][0]) { - $rel= isset($imports[$tokens[$i][1]]) ? $imports[$tokens[$i][1]] : '\\'.$namespace.'\\'.$tokens[$i][1]; + $rel= '\\'.($imports[$tokens[$i][1]] ?? $namespace.'\\'.$tokens[$i][1]); } else if (T_NAME_FULLY_QUALIFIED === $tokens[$i][0]) { $rel= $tokens[$i][1]; } else {