Skip to content

Commit 3acdc8e

Browse files
authored
Merge pull request #9 from xp-lang/feature/anonymous-records
Implement anonymous records
2 parents bdca822 + ff0fa81 commit 3acdc8e

File tree

4 files changed

+124
-58
lines changed

4 files changed

+124
-58
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ jobs:
5454
echo "vendor/xp-framework/compiler/src/test/php" >> composer.pth
5555
5656
- name: Run test suite
57-
run: sh xp-run xp.test.Runner src/test/php
57+
run: sh xp-run xp.test.Runner -r Dots src/test/php

ChangeLog.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ XP records for PHP - ChangeLog
33

44
## ?.?.? / ????-??-??
55

6-
* Removed deprecated `__init()` function in favor of `init { }`
7-
blocks see PR #8 and
6+
## 2.5.0 / 2023-05-27
7+
8+
* Removed support for deprecated `__init()` methods, see PR #8 and
9+
https://github.com/xp-lang/xp-records/releases/tag/v2.2.0
10+
(@thekid)
811

912
## 2.4.0 / 2022-12-04
1013

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

Lines changed: 106 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
InstanceExpression,
88
Literal,
99
Method,
10+
NewClassExpression,
1011
Parameter,
1112
Property,
1213
ReturnStatement,
@@ -24,7 +25,95 @@ public static function inject(&$type, $name, $signature, $body) {
2425
isset($type[$key]) || $type[$key]= new Method(['public'], $name, $signature, [new ReturnStatement($body)]);
2526
}
2627

28+
/** Rewrites a given record and returns it body */
29+
public static function rewrite($node) {
30+
$body= $node->body;
31+
$string= $object= $value= '';
32+
$signature= new Signature([], null);
33+
$constructor= new Method(['public'], '__construct', $signature, []);
34+
foreach ($node->components as $c) {
35+
$l= $c->line;
36+
37+
$modifiers= null === $c->promote ? ['private'] : explode(' ', $c->promote);
38+
$c->promote= null;
39+
$signature->parameters[]= $c;
40+
41+
// Assigment inside constructor
42+
$r= new InstanceExpression(new Variable('this', $l), new Literal($c->name, $l), $l);
43+
$constructor->body[]= new Assignment($r, '=', new Variable($c->name, $l), $l);
44+
45+
// Property declaration + accessor method
46+
$type= $c->variadic ? ($c->type ? new IsArray($c->type) : new IsLiteral('array')) : $c->type;
47+
$body[]= new Property($modifiers, $c->name, $type, null, [], null, $l);
48+
$body[]= new Method(['public'], $c->name, new Signature([], $type), [new ReturnStatement($r, $l)]);
49+
50+
// Code for string representation, hashcode and comparison
51+
$string.= ', '.$c->name.': ".\\util\\Objects::stringOf($this->'.$c->name.')."';
52+
$object.= ', $this->'.$c->name;
53+
$value.= ', $value->'.$c->name;
54+
}
55+
56+
// Create constructor, inlining <init>. Also support deprecated __init() function
57+
if (isset($body['<init>'])) {
58+
foreach ($body['<init>'] as $statement) {
59+
$constructor->body[]= $statement;
60+
}
61+
unset($body['<init>']);
62+
}
63+
$body['__construct()']= $constructor;
64+
65+
// Implement lang.Value
66+
self::inject($body, 'toString', new Signature([], new IsLiteral('string')), new Code(
67+
'"'.strtr(substr($node->name, 1), '\\', '.').'('.substr($string, 2).')"'
68+
));
69+
self::inject($body, 'hashCode', new Signature([], new IsLiteral('string')), new Code(
70+
'md5(\\util\\Objects::hashOf(["'.substr($node->name, 1).'"'.$object.']))'
71+
));
72+
self::inject($body, 'compareTo', new Signature([new Parameter('value', null)], new IsLiteral('int')), new Code(
73+
'$value instanceof self ? \\util\\Objects::compare(['.substr($object, 2).'], ['.substr($value, 2).']) : 1'
74+
));
75+
76+
// Add decomposition
77+
self::inject($body, '__invoke', new Signature([new Parameter('map', new IsLiteral('callable'), new Literal('null'))], null), new Code(
78+
'null === $map ? ['.substr($object, 2).'] : $map('.substr($object, 2).')'
79+
));
80+
81+
return $body;
82+
}
83+
2784
public function setup($language, $emitter) {
85+
86+
// Anonymous records
87+
$new= $language->symbol('new')->nud;
88+
$language->prefix('new', 0, function($parse, $token) use($new) {
89+
if ('record' !== $parse->token->value) return $new($parse, $token);
90+
91+
// Anonymous record syntax: `new record(...) { }`
92+
$parse->forward();
93+
94+
$parse->expecting('(', 'new arguments');
95+
$arguments= $this->arguments($parse);
96+
$parse->expecting(')', 'new arguments');
97+
98+
$parse->expecting('{', 'anonymous record');
99+
$body= $this->typeBody($parse, null);
100+
$parse->expecting('}', 'anonymous record');
101+
102+
$components= [];
103+
foreach ($arguments as $name => $argument) {
104+
$components[]= new Parameter($name, null); // TODO: Infer type
105+
}
106+
107+
$expr= new NewClassExpression(
108+
new RecordDeclaration([], '\\record', $components, null, [], $body),
109+
$arguments,
110+
$token->line
111+
);
112+
$expr->kind= 'newrecord';
113+
return $expr;
114+
});
115+
116+
// Record declaration
28117
$language->stmt('record', function($parse, $token) {
29118
$comment= $parse->comment;
30119
$line= $parse->token->line;
@@ -80,65 +169,28 @@ public function setup($language, $emitter) {
80169
$body['<init>']= $statements;
81170
});
82171

83-
$emitter->transform('record', function($codegen, $node) {
84-
$body= $node->body;
85-
$string= $object= $value= '';
86-
$signature= new Signature([], null);
87-
$constructor= new Method(['public'], '__construct', $signature, []);
88-
foreach ($node->components as $c) {
89-
$l= $c->line;
90-
91-
$modifiers= null === $c->promote ? ['private'] : explode(' ', $c->promote);
92-
$c->promote= null;
93-
$signature->parameters[]= $c;
94-
95-
// Assigment inside constructor
96-
$r= new InstanceExpression(new Variable('this', $l), new Literal($c->name, $l), $l);
97-
$constructor->body[]= new Assignment($r, '=', new Variable($c->name, $l), $l);
98-
99-
// Property declaration + accessor method
100-
$type= $c->variadic ? ($c->type ? new IsArray($c->type) : new IsLiteral('array')) : $c->type;
101-
$body[]= new Property($modifiers, $c->name, $type, null, [], null, $l);
102-
$body[]= new Method(['public'], $c->name, new Signature([], $type), [new ReturnStatement($r, $l)]);
103-
104-
// Code for string representation, hashcode and comparison
105-
$string.= ', '.$c->name.': ".\\util\\Objects::stringOf($this->'.$c->name.')."';
106-
$object.= ', $this->'.$c->name;
107-
$value.= ', $value->'.$c->name;
108-
}
109-
110-
// Create constructor, inlining <init>.
111-
if (isset($body['<init>'])) {
112-
foreach ($body['<init>'] as $statement) {
113-
$constructor->body[]= $statement;
114-
}
115-
unset($body['<init>']);
116-
}
117-
$body['__construct()']= $constructor;
118-
119-
// Implement lang.Value
120-
self::inject($body, 'toString', new Signature([], new IsLiteral('string')), new Code(
121-
'"'.strtr(substr($node->name, 1), '\\', '.').'('.substr($string, 2).')"'
122-
));
123-
self::inject($body, 'hashCode', new Signature([], new IsLiteral('string')), new Code(
124-
'md5(\\util\\Objects::hashOf(["'.substr($node->name, 1).'"'.$object.']))'
125-
));
126-
self::inject($body, 'compareTo', new Signature([new Parameter('value', null)], new IsLiteral('int')), new Code(
127-
'$value instanceof self ? \\util\\Objects::compare(['.substr($object, 2).'], ['.substr($value, 2).']) : 1'
128-
));
129-
$node->implements[]= new IsValue('\\lang\\Value');
130-
131-
// Add decomposition
132-
self::inject($body, '__invoke', new Signature([new Parameter('map', new IsLiteral('callable'), new Literal('null'))], null), new Code(
133-
'null === $map ? ['.substr($object, 2).'] : $map('.substr($object, 2).')'
134-
));
172+
$emitter->transform('newrecord', function($codegen, $node) {
173+
$node->kind= 'newclass';
174+
$node->definition= new ClassDeclaration(
175+
[],
176+
null,
177+
null,
178+
[new IsValue('\\lang\\Value')],
179+
self::rewrite($node->definition),
180+
[],
181+
null,
182+
$node->line
183+
);
184+
return $node;
185+
});
135186

187+
$emitter->transform('record', function($codegen, $node) {
136188
return new ClassDeclaration(
137189
['final'],
138190
$node->name,
139191
$node->parent,
140-
$node->implements,
141-
$body,
192+
array_merge([new IsValue('\\lang\\Value')], $node->implements),
193+
self::rewrite($node),
142194
$node->annotations,
143195
$node->comment,
144196
$node->line

src/test/php/lang/ast/syntax/php/unittest/RecordsTest.class.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?php namespace lang\ast\syntax\php\unittest;
22

3-
use lang\Error;
43
use lang\ast\Errors;
54
use lang\ast\unittest\emit\EmittingTest;
5+
use lang\{Error, Value};
66
use lang\{IllegalArgumentException, XPClass};
77
use test\{Assert, Expect, Test, Values};
88
use util\Objects;
@@ -201,4 +201,15 @@ public function destructure_with_incorrect_mapper() {
201201
$p= $this->type('record <T>(int $x, int $y) { }')->newInstance(1, 10);
202202
$p('not.callable');
203203
}
204+
205+
#[Test]
206+
public function anonymous_record() {
207+
$p= $this->run('class <T> {
208+
public function run() {
209+
return new record(name: "Timm", age: 44) { };
210+
}
211+
}');
212+
Assert::equals('record(name: "Timm", age: 44)', $p->toString());
213+
Assert::instance(Value::class, $p);
214+
}
204215
}

0 commit comments

Comments
 (0)