Skip to content

Commit 5a5322b

Browse files
authored
Merge pull request #6 from xp-lang/feature/casting
Add support for casts
2 parents 2035352 + 8547304 commit 5a5322b

File tree

3 files changed

+160
-13
lines changed

3 files changed

+160
-13
lines changed

src/main/php/lang/ast/syntax/php/Generics.class.php

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@
77
InstanceExpression,
88
InvokeExpression,
99
Literal,
10-
NewExpression,
11-
ScopeExpression
10+
ScopeExpression,
11+
TernaryExpression
1212
};
1313
use lang\ast\syntax\Extension;
1414
use lang\ast\types\{IsArray, IsFunction, IsGeneric, IsMap, IsUnion, IsNullable, IsValue};
1515

16+
/**
17+
* XP Generics extensions
18+
*
19+
* @see https://github.com/xp-framework/rfc/issues/106
20+
* @see https://github.com/xp-framework/rfc/issues/193
21+
* @test lang.ast.syntax.php.unittest.GenericsTest
22+
* @test lang.ast.syntax.php.unittest.CastingTest
23+
*/
1624
class Generics implements Extension {
1725

1826
/**
@@ -46,13 +54,15 @@ public static function components($types) {
4654
*
4755
* @param lang.ast.Type[] $list
4856
* @param lang.ast.Type[] $components
49-
* @return ?string[]
57+
* @param string $prefix
58+
* @param string $suffix
59+
* @return ?string
5060
*/
51-
private static function generics($list, $components) {
61+
private static function generics($list, $components, $prefix= '', $suffix= '') {
5262
$contained= false;
5363
$generics= [];
5464
foreach ($list as $type) {
55-
if ($generic= self::generic($type, $components)) {
65+
if ($generic= self::generic($type, $components, $prefix, $suffix)) {
5666
$contained= true;
5767
$generics[]= $generic;
5868
} else {
@@ -67,25 +77,27 @@ private static function generics($list, $components) {
6777
*
6878
* @param lang.ast.Type $type
6979
* @param lang.ast.Type[] $components
80+
* @param string $prefix
81+
* @param string $suffix
7082
* @return ?string
7183
*/
72-
private static function generic($type, $components) {
84+
private static function generic($type, $components, $prefix= '', $suffix= '') {
7385
if ($type instanceof IsValue && in_array($type, $components)) {
74-
return self::component($type);
86+
return $prefix.self::component($type).$suffix;
7587
} else if ($type instanceof IsNullable) {
76-
if ($generic= self::generic($type->element, $components)) return '?'.$generic;
88+
if ($generic= self::generic($type->element, $components, $prefix, $suffix)) return '?'.$generic;
7789
} else if ($type instanceof IsArray) {
78-
if ($generic= self::generic($type->component, $components)) return $generic.'[]';
90+
if ($generic= self::generic($type->component, $components, $prefix, $suffix)) return $generic.'[]';
7991
} else if ($type instanceof IsMap) {
80-
if ($generic= self::generic($type->value, $components)) return '[:'.$generic.']';
92+
if ($generic= self::generic($type->value, $components, $prefix, $suffix)) return '[:'.$generic.']';
8193
} else if ($type instanceof IsUnion) {
82-
if ($generic= self::generics($type->components, $components)) return implode('|', $generic);
94+
if ($generic= self::generics($type->components, $components, $prefix, $suffix)) return implode('|', $generic);
8395
} else if ($type instanceof IsGeneric) {
84-
if ($generic= self::generics($type->components, $components)) {
96+
if ($generic= self::generics($type->components, $components, $prefix, $suffix)) {
8597
return $type->base->name().'<'.implode(', ', $generic).'>';
8698
}
8799
} else if ($type instanceof IsFunction) {
88-
if ($generic= self::generics(array_merge([$type->returns], $type->signature), $components)) {
100+
if ($generic= self::generics(array_merge([$type->returns], $type->signature), $components, $prefix, $suffix)) {
89101
$return= array_shift($generic);
90102
return '(function('.implode(', ', $generic).'): '.$return.')';
91103
}
@@ -177,6 +189,8 @@ public static function type($type, $values) {
177189
self::annotate($method, self::method($method, $type->name->components));
178190
}
179191

192+
// Ensure class name is emitted as its base type
193+
$type->name= new IsGenericDeclaration($type->name);
180194
return $type;
181195
}
182196

@@ -298,5 +312,19 @@ public function setup($language, $emitter) {
298312
}
299313
return $node;
300314
});
315+
316+
$emitter->transform('cast', function($codegen, $node) {
317+
$type= $codegen->scope[0]->type;
318+
if ($type->name instanceof IsGenericDeclaration) {
319+
if ($generic= self::generic($node->type, $type->name->components(), '{$_G[\'', '\']}')) {
320+
return new TernaryExpression(
321+
new Code('($_G ?? $_G= self::$__generic)'),
322+
new InvokeExpression(new Literal('cast'), [$node->expression, new Literal('"'.$generic.'"')]),
323+
new Literal('null')
324+
);
325+
}
326+
}
327+
return $node;
328+
});
301329
}
302330
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php namespace lang\ast\syntax\php;
2+
3+
use lang\ast\Type;
4+
5+
class IsGenericDeclaration extends Type {
6+
private $type;
7+
8+
public function __construct($type) {
9+
$this->type= $type;
10+
}
11+
12+
/** @return string */
13+
public function name() { return $this->type->base->name(); }
14+
15+
/** @return string */
16+
public function literal() { return $this->type->base->literal(); }
17+
18+
/** @return parent[] */
19+
public function components() { return $this->type->components; }
20+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php namespace lang\ast\syntax\php\unittest;
2+
3+
use lang\ast\unittest\emit\EmittingTest;
4+
use lang\reflect\TargetInvocationException;
5+
use lang\{ArrayType, Primitive};
6+
use test\{Assert, Expect, Test, Values};
7+
8+
class CastingTest extends EmittingTest {
9+
10+
/**
11+
* Invokes static fixture method with the given arguments. Unwraps
12+
* any exception thrown from `TargetInvocationException`.
13+
*
14+
* @param lang.XPClass $type
15+
* @param var[] $arguments
16+
* @return var
17+
*/
18+
private function invokeFixture($type, $arguments= []) {
19+
try {
20+
return $type->getMethod('fixture')->invoke(null, $arguments);
21+
} catch (TargetInvocationException $e) {
22+
throw $e->getCause();
23+
}
24+
}
25+
26+
#[Test, Values([1, null, [[]]])]
27+
public function regular_cast($value) {
28+
$t= $this->type('class %T<V> {
29+
public static function fixture($arg) {
30+
return (array)$arg;
31+
}
32+
}');
33+
34+
Assert::equals(
35+
(array)$value,
36+
$this->invokeFixture($t->newGenericType([Primitive::$STRING]), [$value])
37+
);
38+
}
39+
40+
#[Test, Values([1, '', 'Test'])]
41+
public function generic_cast($value) {
42+
$t= $this->type('class %T<V> {
43+
public static function fixture($arg) {
44+
return (V)$arg;
45+
}
46+
}');
47+
48+
Assert::equals(
49+
Primitive::$STRING->cast($value),
50+
$this->invokeFixture($t->newGenericType([Primitive::$STRING]), [$value])
51+
);
52+
}
53+
54+
#[Test, Values([[[]], [[1, 2, 3]]])]
55+
public function generic_array_cast($value) {
56+
$t= $this->type('class %T<V> {
57+
public static function fixture($arg) {
58+
return (array<V>)$arg;
59+
}
60+
}');
61+
62+
Assert::equals(
63+
(new ArrayType(Primitive::$STRING))->cast($value),
64+
$this->invokeFixture($t->newGenericType([Primitive::$STRING]), [$value])
65+
);
66+
}
67+
68+
#[Test, Values([1, null, '', 'Test'])]
69+
public function generic_nullable_cast($value) {
70+
$t= $this->type('class %T<V> {
71+
public static function fixture($arg) {
72+
return (?V)$arg;
73+
}
74+
}');
75+
76+
Assert::equals(
77+
Primitive::$STRING->cast($value),
78+
$this->invokeFixture($t->newGenericType([Primitive::$STRING]), [$value])
79+
);
80+
}
81+
82+
#[Test]
83+
public function casting_used_for_coercion() {
84+
$t= $this->type('class %T<T> {
85+
private $begin, $end;
86+
87+
public function __construct($range) {
88+
[$this->begin, $this->end]= (array<T>)$range;
89+
}
90+
91+
public function begin(): T { return $this->begin; }
92+
93+
public function end(): T { return $this->end; }
94+
}');
95+
96+
$range= $t->newGenericType([Primitive::$INT])->newInstance(['1', '10']);
97+
Assert::equals([1, 10], [$range->begin(), $range->end()]);
98+
}
99+
}

0 commit comments

Comments
 (0)