Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions src/main/php/lang.base.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
123 changes: 69 additions & 54 deletions src/main/php/lang/GenericTypes.class.php
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
<?php namespace lang;

use ReflectionClass;

/**
* 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) {
return new XPClass(new \ReflectionClass($this->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']))) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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'])) {
Expand All @@ -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"'.
');'
Expand All @@ -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);
Expand All @@ -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 {
Expand All @@ -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;
}
Expand All @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion src/test/php/lang/unittest/CreateTest.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace lang\unittest;

use lang\{IllegalArgumentException, XPClass};
use lang\{IllegalArgumentException, IllegalStateException, XPClass};
use test\{Assert, Expect, Test};

class CreateTest {
Expand Down Expand Up @@ -29,6 +29,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<string>');
}
Expand Down
45 changes: 45 additions & 0 deletions src/test/php/lang/unittest/GenericsTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<?string>')
->append('Hello')
->append(null)
;

Assert::equals(['Hello', null], $fixture->elements);
}

#[Test]
public function pass_union() {
$fixture= create('new lang.unittest.ListOf<int|float>')
->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<string>', 'Hello')->extend($arguments);
Expand All @@ -93,4 +113,29 @@ public function pass_invalid($arguments) {
public function pass_invalid_varargs($arguments) {
create('new lang.unittest.ListOf<string>', ...$arguments);
}

#[Test]
public function invoke_generic_method() {
$fixture= create('new lang.unittest.ListOf<string>', 'Hello', 'World!');
$mapped= $fixture->{'map<int>'}('strlen');

Assert::instance('lang.unittest.ListOf<int>', $mapped);
Assert::equals([5, 6], $mapped->elements);
}

#[Test]
public function generic_method_components() {
$fixture= create('new lang.unittest.Lookup<string, string>');
$fixture->put('greeting', 'Hello');
$fixture->put('person', 'Tester');
$mapped= $fixture->{'map<int>'}('strlen');

Assert::instance('lang.unittest.Lookup<string, int>', $mapped);
Assert::equals([5, 6], $mapped->values());
}

#[Test, Expect(IllegalArgumentException::class)]
public function generic_method_invalid_argument() {
create('new lang.unittest.ListOf<string>')->{'map<int>'}(null);
}
}
Loading
Loading