支持 Iterator/IteratorAggregate 对象 foreach 操作

pull/1/head
韩天峰 8 months ago
parent 9ca07388bc
commit c9d6e93f40
  1. 8
      src/Php/AstNodeType.php
  2. 93
      src/Php/CompilerBase.php
  3. 39
      src/Php/Translator.php
  4. 199
      tests/core/classes/iterators_001.phpt
  5. 105
      tests/core/classes/iterators_002.phpt

@ -3,11 +3,17 @@
namespace PhpAot\Php; namespace PhpAot\Php;
use PhpParser\Node\Expr; use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
trait AstNodeType trait AstNodeType
{ {
protected function isVarExpr(Expr $expr): bool protected function isVarExpr(NodeAbstract $expr): bool
{ {
return $expr instanceof Expr\Variable; return $expr instanceof Expr\Variable;
} }
protected function isPropertyFetch(NodeAbstract $expr): bool
{
return $expr instanceof Expr\PropertyFetch;
}
} }

@ -134,7 +134,6 @@ class CompilerBase extends \PhpAot\Core\Translator
protected array $beforeStmtLines = []; protected array $beforeStmtLines = [];
protected array $afterStmtLines = []; protected array $afterStmtLines = [];
protected bool $inLoop = false; protected bool $inLoop = false;
protected bool $inSwitch = false;
protected bool $stubFile = false; protected bool $stubFile = false;
protected bool $stubFileIncluded = false; protected bool $stubFileIncluded = false;
protected Parser $parser; protected Parser $parser;
@ -207,11 +206,12 @@ class CompilerBase extends \PhpAot\Core\Translator
return isset($this->nativeFunctions[$name]); return isset($this->nativeFunctions[$name]);
} }
protected function resetScope(): void protected function resetFunction(): void
{ {
$this->localVars = []; $this->localVars = [];
$this->arguments = []; $this->arguments = [];
$this->tmpVarIndex = 0; $this->tmpVarIndex = 0;
$this->inLoop = false;
} }
protected function resetFile(): void protected function resetFile(): void
@ -261,7 +261,7 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function parseFunction(Node\FunctionLike $v): string protected function parseFunction(Node\FunctionLike $v): string
{ {
$this->resetScope(); $this->resetFunction();
$this->function = $this->parseIdentifier($v->name); $this->function = $this->parseIdentifier($v->name);
$name = $this->getFunctionName($v); $name = $this->getFunctionName($v);
if (isset($this->nativeFunctions[$name])) { if (isset($this->nativeFunctions[$name])) {
@ -403,9 +403,26 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->getIndent() . '// ' . $class . ' [' . $v->getStartLine() . ':' . $v->getEndLine() . ']'; return $this->getIndent() . '// ' . $class . ' [' . $v->getStartLine() . ':' . $v->getEndLine() . ']';
} }
/**
* 在 for/foreach 等包含子语句的语句,之前检查当前待添加的代码是否为空,
* 如果不为空,需要将语句追加到 {} 作用域符号之前
* @return string
*/
protected function parseBeforeStmtLines(): string
{
if ($this->beforeStmtLines) {
$code = implode(PHP_EOL, $this->beforeStmtLines);
$this->beforeStmtLines = [];
return $code . PHP_EOL;
} else {
return '';
}
}
protected function parseStmts(array $stmts): string protected function parseStmts(array $stmts): string
{ {
$lines = []; $lines = [];
$inLoopTop = $this->inLoop;
foreach ($stmts as $v) { foreach ($stmts as $v) {
$class = $v->getType(); $class = $v->getType();
$this->beforeStmtLines = []; $this->beforeStmtLines = [];
@ -426,27 +443,27 @@ class CompilerBase extends \PhpAot\Core\Translator
case 'Stmt_For': case 'Stmt_For':
$this->inLoop = true; $this->inLoop = true;
$result = $this->parseFor($v); $result = $this->parseFor($v);
$this->inLoop = false; $this->inLoop = $inLoopTop;
break; break;
case 'Stmt_Foreach': case 'Stmt_Foreach':
$this->inLoop = true; $this->inLoop = true;
$result = $this->parseForeach($v); $result = $this->parseForeach($v);
$this->inLoop = false; $this->inLoop = $inLoopTop;
break; break;
case 'Stmt_Switch': case 'Stmt_Switch':
$this->inSwitch = true; $this->inLoop = true;
$result = $this->parseSwitch($v); $result = $this->parseSwitch($v);
$this->inSwitch = false; $this->inLoop = $inLoopTop;
break; break;
case 'Stmt_While': case 'Stmt_While':
$this->inLoop = true; $this->inLoop = true;
$result = $this->parseWhile($v); $result = $this->parseWhile($v);
$this->inLoop = false; $this->inLoop = $inLoopTop;
break; break;
case 'Stmt_Do': case 'Stmt_Do':
$this->inLoop = true; $this->inLoop = true;
$result = $this->parseDo($v); $result = $this->parseDo($v);
$this->inLoop = false; $this->inLoop = $inLoopTop;
break; break;
case 'Stmt_If': case 'Stmt_If':
$result = $this->parseIf($v); $result = $this->parseIf($v);
@ -1115,7 +1132,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$name = $this->parseIdentifier($left); $name = $this->parseIdentifier($left);
$type = $this->detectExprType($expr->expr); $type = $this->detectExprType($expr->expr);
// for 循环的变量声明,必须在循环体之外,不能创建 scope tmp var // for 循环的变量声明,必须在循环体之外,不能创建 scope tmp var
if ($this->hasVar($name)) { if (!$this->hasVar($name)) {
$this->addLocalVar($name, $type); $this->addLocalVar($name, $type);
} }
$code .= $name . ' = ' . '(' . $this->parseIdentifier($expr->expr) . ');'; $code .= $name . ' = ' . '(' . $this->parseIdentifier($expr->expr) . ');';
@ -1123,6 +1140,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$list_cond[] = $this->parseExpr($expr); $list_cond[] = $this->parseExpr($expr);
} }
$code .= $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'for (;'; $code .= 'for (;';
$code .= implode(', ', $list_cond); $code .= implode(', ', $list_cond);
$code .= '; '; $code .= '; ';
@ -1418,7 +1436,14 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function parsePostInc($expr): string protected function parsePostInc($expr): string
{ {
return $this->parseIdentifier($expr->var) . '++'; if ($this->isVarExpr($expr->var)) {
return $this->parseIdentifier($expr->var) . '++';
} elseif ($this->isPropertyFetch($expr->var)) {
$obj = $this->parseIdentifier($expr->var->var);
$prop = $this->identifierToStr($expr->var->name);
return $obj . '.setProperty(' . $prop . ', ' . $obj. '.getProperty(' . $prop . ') + 1)';
}
abort($expr, "Post-increment operator is not supported for non-variable expressions");
} }
protected function parsePostDec($expr): string protected function parsePostDec($expr): string
@ -1482,7 +1507,8 @@ class CompilerBase extends \PhpAot\Core\Translator
{ {
$cond = $this->parseExpr($v->cond); $cond = $this->parseExpr($v->cond);
$code = 'if (' . $cond . ') {' . PHP_EOL; $code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'if (' . $cond . ') {' . PHP_EOL;
$this->indentLevel++; $this->indentLevel++;
$code .= $this->parseStmts($v->stmts); $code .= $this->parseStmts($v->stmts);
$this->indentLevel--; $this->indentLevel--;
@ -1549,7 +1575,8 @@ class CompilerBase extends \PhpAot\Core\Translator
$cond = $this->parseExpr($v->cond); $cond = $this->parseExpr($v->cond);
$stmts = $v->stmts; $stmts = $v->stmts;
$code = 'while (' . $cond . ') {' . PHP_EOL; $code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'while (' . $cond . ') {' . PHP_EOL;
$this->indentLevel++; $this->indentLevel++;
$code .= $this->parseStmts($stmts); $code .= $this->parseStmts($stmts);
$this->indentLevel--; $this->indentLevel--;
@ -1670,8 +1697,8 @@ class CompilerBase extends \PhpAot\Core\Translator
{ {
$stmts = $v->stmts; $stmts = $v->stmts;
$cond = $this->parseExpr($v->cond); $cond = $this->parseExpr($v->cond);
$code = $this->parseBeforeStmtLines() . PHP_EOL;
$code = 'do {' . PHP_EOL; $code .= 'do {' . PHP_EOL;
$this->indentLevel++; $this->indentLevel++;
$code .= $this->parseStmts($stmts); $code .= $this->parseStmts($stmts);
$this->indentLevel--; $this->indentLevel--;
@ -1892,7 +1919,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return 'php::exit(' . $this->parseIdentifier($node->expr) . ')'; return 'php::exit(' . $this->parseIdentifier($node->expr) . ')';
} }
protected function parseUnset(Node $node): string protected function parseUnset(Node\Stmt\Unset_ $node): string
{ {
$vars = $node->vars; $vars = $node->vars;
$lines = []; $lines = [];
@ -1916,7 +1943,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return implode(PHP_EOL . $this->getIndent(), $lines); return implode(PHP_EOL . $this->getIndent(), $lines);
} }
protected function parsePropertyFetch(Node $expr): string protected function parsePropertyFetch(Node\Expr\PropertyFetch $expr): string
{ {
return $this->convertToObject($expr->var) . '.getProperty("' . $this->parseIdentifier($expr->name) . '")'; return $this->convertToObject($expr->var) . '.getProperty("' . $this->parseIdentifier($expr->name) . '")';
} }
@ -1958,18 +1985,28 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($node->byRef) { if ($node->byRef) {
$this->fatalError($node, 'Cannot use & with foreach'); $this->fatalError($node, 'Cannot use & with foreach');
} }
if ($node->keyVar) { if ($this->isVarExpr($node->expr)) {
$keyVar = $this->parseIdentifier($node->keyVar); $name = $this->parseIdentifier($node->expr);
if ($this->hasVar($name)) {
$type = $this->getVarType($name);
if ($type === self::TYPE_OBJECT) {
return $this->parseForeachObject($node);
}
}
} }
$valueVar = $this->parseIdentifier($node->valueVar);
$iteratorVar = $this->genTmpVarName(); $iteratorVar = $this->genTmpVarName();
$stmts = $node->stmts; $stmts = $node->stmts;
$code = ''; $code = '';
if ($node->keyVar) {
$keyVar = $this->parseIdentifier($node->keyVar);
}
$valueVar = $this->parseIdentifier($node->valueVar);
$expr = $this->parseIdentifier($node->expr); $expr = $this->parseIdentifier($node->expr);
$code .= self::TYPE_ARRAY . " $iteratorVar = " . $expr . ';' . PHP_EOL; $code .= self::TYPE_ARRAY . " $iteratorVar = " . $expr . ';' . PHP_EOL;
$code .= $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'for (auto iter = ' . $iteratorVar . '.begin(); iter != ' . $iteratorVar . '.end(); ++iter) {' . PHP_EOL; $code .= 'for (auto iter = ' . $iteratorVar . '.begin(); iter != ' . $iteratorVar . '.end(); ++iter) {' . PHP_EOL;
$this->indentLevel++; $this->indentLevel++;
@ -2035,9 +2072,10 @@ class CompilerBase extends \PhpAot\Core\Translator
// 保存作用域,switch 可能会解析失败,在这个过程中会增加变量,需重置 // 保存作用域,switch 可能会解析失败,在这个过程中会增加变量,需重置
$localVars = $this->localVars; $localVars = $this->localVars;
$code = $this->parseBeforeStmtLines() . PHP_EOL;
if ($type === self::TYPE_INT or $type === self::TYPE_FLOAT) { if ($type === self::TYPE_INT or $type === self::TYPE_FLOAT) {
$code = 'switch (' . $tmp_var . ') {' . PHP_EOL; $code .= 'switch (' . $tmp_var . ') {' . PHP_EOL;
$this->indentLevel++; $this->indentLevel++;
foreach ($v->cases as $case) { foreach ($v->cases as $case) {
if (empty($case->cond)) { if (empty($case->cond)) {
@ -2105,7 +2143,7 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function parseBreak(mixed $v): string protected function parseBreak(mixed $v): string
{ {
if (!$this->inLoop and !$this->inSwitch) { if (!$this->inLoop) {
$this->fatalError($v, 'Cannot break outside loop'); $this->fatalError($v, 'Cannot break outside loop');
} }
$num = $v->num; $num = $v->num;
@ -2208,7 +2246,7 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function convertToObject(Node $object): string protected function convertToObject(Node $object): string
{ {
$id = $this->parseIdentifier($object); $id = $this->parseIdentifier($object);
if (!$this->hasVar($id)) { if ($this->isVarExpr($object) and !$this->hasVar($id)) {
$this->addLocalVar($id, self::TYPE_OBJECT); $this->addLocalVar($id, self::TYPE_OBJECT);
return $id; return $id;
} }
@ -2245,7 +2283,7 @@ class CompilerBase extends \PhpAot\Core\Translator
abort($expr); abort($expr);
} }
protected function parseMethodCall(mixed $expr): string protected function parseMethodCall(Node\Expr\MethodCall $expr): string
{ {
$object = $this->convertToObject($expr->var); $object = $this->convertToObject($expr->var);
$method = $this->parseIdentifier($expr->name); $method = $this->parseIdentifier($expr->name);
@ -2333,7 +2371,8 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function parseTryCatch(mixed $v): string protected function parseTryCatch(mixed $v): string
{ {
$code = 'zend_try {'; $code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'zend_try {';
$stmts = $v->stmts; $stmts = $v->stmts;
$code .= PHP_EOL; $code .= PHP_EOL;
@ -2375,6 +2414,8 @@ class CompilerBase extends \PhpAot\Core\Translator
} }
$code = $this->getIndent() . $var . ' = ' . $exVar . ';' . PHP_EOL; $code = $this->getIndent() . $var . ' = ' . $exVar . ';' . PHP_EOL;
$code .= $this->parseBeforeStmtLines() . PHP_EOL;
$code .= $this->getIndent() . 'if (' . $var . ' && '; $code .= $this->getIndent() . 'if (' . $var . ' && ';
foreach ($types as $type) { foreach ($types as $type) {
$code .= 'php::instanceOf(' . $var . ', "' . $this->parseIdentifier($type) . '")'; $code .= 'php::instanceOf(' . $var . ', "' . $this->parseIdentifier($type) . '")';
@ -2566,6 +2607,4 @@ class CompilerBase extends \PhpAot\Core\Translator
} }
abort($expr); abort($expr);
} }
} }

@ -5,6 +5,7 @@ namespace PhpAot\Php;
use MJS\TopSort\Implementations\StringSort; use MJS\TopSort\Implementations\StringSort;
use PhpParser\Modifiers; use PhpParser\Modifiers;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\NodeTraverser; use PhpParser\NodeTraverser;
class Translator extends Preprocessor class Translator extends Preprocessor
@ -779,7 +780,7 @@ class Translator extends Preprocessor
} }
} }
private function parseClassMethod(Node\Stmt\ClassMethod $v, array &$methodCodes): void protected function parseClassMethod(Node\Stmt\ClassMethod $v, array &$methodCodes): void
{ {
$name = $this->getMethodName($v); $name = $this->getMethodName($v);
$this->method = $name; $this->method = $name;
@ -794,7 +795,7 @@ class Translator extends Preprocessor
$this->method = ''; $this->method = '';
} }
private function parseIdentifierList(array $implements): array protected function parseIdentifierList(array $implements): array
{ {
$list = []; $list = [];
foreach ($implements as $implement) { foreach ($implements as $implement) {
@ -803,7 +804,7 @@ class Translator extends Preprocessor
return $list; return $list;
} }
private function parseInterface(Node\Stmt\Interface_ $v): void protected function parseInterface(Node\Stmt\Interface_ $v): void
{ {
$name = $this->parseIdentifier($v->name); $name = $this->parseIdentifier($v->name);
$this->interface = $name; $this->interface = $name;
@ -812,4 +813,36 @@ class Translator extends Preprocessor
$this->interfaces[$interfaceName] = $this->interfaceDef; $this->interfaces[$interfaceName] = $this->interfaceDef;
$this->interfacesDefineInFile[$interfaceName] = $this->interfaceDef; $this->interfacesDefineInFile[$interfaceName] = $this->interfaceDef;
} }
protected function parseForeachObject(Foreach_ $node): string
{
$obj = $this->parseIdentifier($node->expr);
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_OBJECT);
$code = 'if (' . $obj . '.instanceOf("IteratorAggregate")) {' . PHP_EOL;
$code .= $this->getIndent() . $tmpVar . ' = ' . $obj . '.exec("getIterator");' . PHP_EOL . '}' . PHP_EOL;
$code .= 'else if (' . $obj . '.instanceOf("Iterator")) {' . PHP_EOL;
$code .= $this->getIndent() . $tmpVar . ' = ' . $obj . ';' . PHP_EOL . '}'. PHP_EOL;
$code .= 'if (' . $tmpVar . ') {'. PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() .$tmpVar . '.exec("rewind");' . PHP_EOL;
$code .= $this->getIndent() .'while (' . $tmpVar . '.exec("valid")) {' . PHP_EOL;
$this->indentLevel++;
$valueVar = $this->parseIdentifier($node->valueVar);
$code .= $this->getIndent() . self::TYPE_VAR . ' ' . $valueVar . ' = ' . $tmpVar . '.exec("current");' . PHP_EOL;
if ($node->keyVar) {
$keyVar = $this->parseIdentifier($node->keyVar);
$code .= $this->getIndent() . self::TYPE_VAR . ' ' . $keyVar . ' = ' . $tmpVar . '.exec("key");' . PHP_EOL;
}
$code .= $this->parseStmts($node->stmts);
$code .= $this->getIndent() . $tmpVar . '.exec("next");' . PHP_EOL;
$code .= '}'. PHP_EOL;
$this->indentLevel--;
$this->indentLevel--;
$code .= '}'. PHP_EOL;
return $code;
}
} }

@ -0,0 +1,199 @@
--TEST--
ZE2 iterators and foreach
--FILE--
<?php
class c_iter implements Iterator {
private $obj;
private $num = 0;
function __construct($obj) {
echo __METHOD__ . "\n";
$this->num = 0;
$this->obj = $obj;
}
function rewind(): void {
}
function valid(): bool {
$more = $this->num < $this->obj->max;
echo __METHOD__ . ' = ' .($more ? 'true' : 'false') . "\n";
return $more;
}
function current(): mixed {
echo __METHOD__ . "\n";
return $this->num;
}
function next(): void {
echo __METHOD__ . "\n";
$this->num++;
}
function key(): mixed {
echo __METHOD__ . "\n";
switch($this->num) {
case 0: return "1st";
case 1: return "2nd";
case 2: return "3rd";
default: return "???";
}
}
}
class c implements IteratorAggregate {
public $max = 3;
function getIterator(): Traversable {
echo __METHOD__ . "\n";
return new c_iter($this);
}
}
function main() {
echo "===Array===\n";
$a = array(0,1,2);
foreach($a as $v) {
echo "array:$v\n";
}
echo "===Manual===\n";
$t = new c();
for ($iter = $t->getIterator(); $iter->valid(); $iter->next()) {
echo $iter->current() . "\n";
}
echo "===foreach/std===\n";
foreach($t as $v) {
echo "object:$v\n";
}
echo "===foreach/rec===\n";
foreach($t as $v) {
foreach($t as $w) {
echo "double:$v:$w\n";
}
}
echo "===foreach/key===\n";
foreach($t as $i => $v) {
echo "object:$i=>$v\n";
}
print "Done\n";
}
?>
--EXPECT--
===Array===
array:0
array:1
array:2
===Manual===
c::getIterator
c_iter::__construct
c_iter::valid = true
c_iter::current
0
c_iter::next
c_iter::valid = true
c_iter::current
1
c_iter::next
c_iter::valid = true
c_iter::current
2
c_iter::next
c_iter::valid = false
===foreach/std===
c::getIterator
c_iter::__construct
c_iter::valid = true
c_iter::current
object:0
c_iter::next
c_iter::valid = true
c_iter::current
object:1
c_iter::next
c_iter::valid = true
c_iter::current
object:2
c_iter::next
c_iter::valid = false
===foreach/rec===
c::getIterator
c_iter::__construct
c_iter::valid = true
c_iter::current
c::getIterator
c_iter::__construct
c_iter::valid = true
c_iter::current
double:0:0
c_iter::next
c_iter::valid = true
c_iter::current
double:0:1
c_iter::next
c_iter::valid = true
c_iter::current
double:0:2
c_iter::next
c_iter::valid = false
c_iter::next
c_iter::valid = true
c_iter::current
c::getIterator
c_iter::__construct
c_iter::valid = true
c_iter::current
double:1:0
c_iter::next
c_iter::valid = true
c_iter::current
double:1:1
c_iter::next
c_iter::valid = true
c_iter::current
double:1:2
c_iter::next
c_iter::valid = false
c_iter::next
c_iter::valid = true
c_iter::current
c::getIterator
c_iter::__construct
c_iter::valid = true
c_iter::current
double:2:0
c_iter::next
c_iter::valid = true
c_iter::current
double:2:1
c_iter::next
c_iter::valid = true
c_iter::current
double:2:2
c_iter::next
c_iter::valid = false
c_iter::next
c_iter::valid = false
===foreach/key===
c::getIterator
c_iter::__construct
c_iter::valid = true
c_iter::current
c_iter::key
object:1st=>0
c_iter::next
c_iter::valid = true
c_iter::current
c_iter::key
object:2nd=>1
c_iter::next
c_iter::valid = true
c_iter::current
c_iter::key
object:3rd=>2
c_iter::next
c_iter::valid = false
Done

@ -0,0 +1,105 @@
--TEST--
ZE2 iterators and break
--FILE--
<?php
class c_iter implements Iterator {
private $obj;
private $num = 0;
function __construct($obj) {
echo __METHOD__ . "\n";
$this->obj = $obj;
}
function rewind(): void {
echo __METHOD__ . "\n";
$this->num = 0;
}
function valid(): bool {
$more = $this->num < $this->obj->max;
echo __METHOD__ . ' = ' .($more ? 'true' : 'false') . "\n";
return $more;
}
function current(): mixed {
echo __METHOD__ . "\n";
return $this->num;
}
function next(): void {
echo __METHOD__ . "\n";
$this->num++;
}
function key(): mixed {
echo __METHOD__ . "\n";
switch($this->num) {
case 0: return "1st";
case 1: return "2nd";
case 2: return "3rd";
default: return "???";
}
}
function __destruct() {
}
}
class c implements IteratorAggregate {
public $max = 3;
function getIterator(): Traversable {
echo __METHOD__ . "\n";
return new c_iter($this);
}
function __destruct() {
}
}
function main() {
$t = new c();
foreach($t as $k => $v) {
foreach($t as $w) {
echo "double:$v:$w\n";
break;
}
}
unset($t);
print "Done\n";
}
?>
--EXPECT--
c::getIterator
c_iter::__construct
c_iter::rewind
c_iter::valid = true
c_iter::current
c_iter::key
c::getIterator
c_iter::__construct
c_iter::rewind
c_iter::valid = true
c_iter::current
double:0:0
c_iter::next
c_iter::valid = true
c_iter::current
c_iter::key
c::getIterator
c_iter::__construct
c_iter::rewind
c_iter::valid = true
c_iter::current
double:1:0
c_iter::next
c_iter::valid = true
c_iter::current
c_iter::key
c::getIterator
c_iter::__construct
c_iter::rewind
c_iter::valid = true
c_iter::current
double:2:0
c_iter::next
c_iter::valid = false
Done
Loading…
Cancel
Save