diff --git a/src/main/php/lang.base.php b/src/main/php/lang.base.php index fef176bd3..134ea76ad 100755 --- a/src/main/php/lang.base.php +++ b/src/main/php/lang.base.php @@ -323,27 +323,28 @@ 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); - $typeargs= \lang\Type::forNames(substr($spec, $b+ 1, strrpos($spec, '>')- $b- 1)); + $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"))); + } 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($type, $b + 1, strrpos($type, '>') - $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/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index f4b52b855..3b056adf8 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -1,31 +1,22 @@ newType0($base, $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']))) { @@ -155,10 +146,10 @@ 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; } @@ -169,22 +160,29 @@ public function newType0($base, $arguments) { $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); + $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,14 +191,24 @@ 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]) { break; } - } else if (2 === $state[0]) { // Method declaration + } else if (T_METHOD_C === $state[0]) { if ('(' === $tokens[$i][0]) { $braces++; + if (1 === $braces && $typeargs) { + $src.= '($__T,'; + continue; + } } else if (')' === $tokens[$i][0]) { $braces--; if (0 === $braces) { @@ -214,12 +222,12 @@ 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]; } - } 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'])) { @@ -236,34 +244,24 @@ 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.= '{'; - $annotations= $generic ? $generic[0]->getArguments() : []; if (isset($annotations['return'])) { $meta[1][$m][DETAIL_RETURNS]= strtr($annotations['return'], $placeholders); } if (isset($annotations['params'])) { - $generic= []; - foreach (Type::split($annotations['params']) as $j => $placeholder) { - if ('' === ($replaced= strtr($placeholder, $placeholders))) { - $generic[$j]= null; - } else { - $meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced; - $generic[$j]= $replaced; - } + $replace= $rewrite= ['...' => '[]'] + $placeholders; + foreach ($typeargs as $placeholder => $p) { + $replace[$placeholder]= "\$__T[{$p}]"; } - foreach ($generic as $j => $type) { - if (null === $type) { - continue; - } else if ('...' === substr($type, -3)) { - $verify= substr($generic[$j], 0, -3).'[]'; - } else { - $verify= $generic[$j]; - } + foreach (Type::split($annotations['params']) as $j => $placeholder) { + 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(\''.$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"'. ');' @@ -272,17 +270,34 @@ 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]) { $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 ('"' === $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]) + ? 'self::$__generic["'.$v.'"]' : + (isset($typeargs[$v]) ? '$__T['.$typeargs[$v].']' : $tokens[$i][1]) + ; + 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) + } else if (T_IMPLEMENTS === $state[0]) { // Implements (class), Extends (interface) if ('{' === $tokens[$i]) { array_shift($state); array_shift($state); @@ -295,9 +310,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 { @@ -310,7 +325,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; } @@ -322,7 +337,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/CreateTest.class.php b/src/test/php/lang/unittest/CreateTest.class.php index ef2ff855f..b75a836df 100755 --- a/src/test/php/lang/unittest/CreateTest.class.php +++ b/src/test/php/lang/unittest/CreateTest.class.php @@ -1,6 +1,6 @@ '); } diff --git a/src/test/php/lang/unittest/GenericsTest.class.php b/src/test/php/lang/unittest/GenericsTest.class.php index fb6987ef3..27a515689 100755 --- a/src/test/php/lang/unittest/GenericsTest.class.php +++ b/src/test/php/lang/unittest/GenericsTest.class.php @@ -84,6 +84,26 @@ 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] + 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); @@ -93,4 +113,29 @@ 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); + } + + #[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); + } } \ 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..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,11 +35,24 @@ 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; } + /** + * 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', return: 'self')] + public function map($map) { + $m= create("new self<$M>"); + foreach ($this->elements as $element) { + $m->elements[]= $map($element); + } + return $m; + } + /** * Returns a list of all elements * 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 *