diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 6efd95307..00fb92c9a 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -11,6 +11,11 @@ 'array_syntax' => ['syntax' => 'short'], 'single_line_empty_body' => true, 'statement_indentation' => false, + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_constants' => true, + 'import_functions' => true, + ], ]; CONFIG->setRules(RULES); diff --git a/README.md b/README.md index 8ceee71ff..556461a18 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,6 @@ Already implemented: * see [tests](tests/Functional/files/) -To be implemented: - -* channels and select statements -* type assertions -* generics - ## Development install dependencies: diff --git a/composer.json b/composer.json index 313fd5c7c..ac5907b9c 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "php": "^8.2", - "tuqqu/go-parser": "^0.4.4" + "tuqqu/go-parser": "^0.5" }, "require-dev": { "symfony/var-dumper": "^6", diff --git a/src/Builtin/BuiltinFunc/Delete.php b/src/Builtin/BuiltinFunc/Delete.php index 03bfca5eb..864d8c56a 100644 --- a/src/Builtin/BuiltinFunc/Delete.php +++ b/src/Builtin/BuiltinFunc/Delete.php @@ -30,7 +30,7 @@ public function __invoke(Argv $argv): VoidValue $m->delete($key); - return new VoidValue(); + return VoidValue::get(); } public function name(): string diff --git a/src/Builtin/BuiltinFunc/Print_.php b/src/Builtin/BuiltinFunc/Print_.php index 0fa2b4ed6..63173f0c0 100644 --- a/src/Builtin/BuiltinFunc/Print_.php +++ b/src/Builtin/BuiltinFunc/Print_.php @@ -30,7 +30,7 @@ public function __invoke(Argv $argv): VoidValue $this->stderr->write(implode('', $output)); - return new VoidValue(); + return VoidValue::get(); } public function name(): string diff --git a/src/Builtin/BuiltinFunc/Println.php b/src/Builtin/BuiltinFunc/Println.php index e93ab32ea..7f5920aca 100644 --- a/src/Builtin/BuiltinFunc/Println.php +++ b/src/Builtin/BuiltinFunc/Println.php @@ -30,7 +30,7 @@ public function __invoke(Argv $argv): VoidValue $this->stderr->writeln(implode(' ', $output)); - return new VoidValue(); + return VoidValue::get(); } public function name(): string diff --git a/src/Error/ParserError.php b/src/Error/ParserError.php index 87c2a66e9..cf3bb21ea 100644 --- a/src/Error/ParserError.php +++ b/src/Error/ParserError.php @@ -24,7 +24,7 @@ public static function fromInternalParserError(Error $error): self match (true) { $error instanceof LexError => $error->pos, $error instanceof SyntaxError => $error->pos, - default => throw new InternalError(sprintf('Unknown error type %s', $error::class)), + default => throw new InternalError(sprintf('unknown error type %s', $error::class)), }, (string) $error, ); diff --git a/src/Error/RuntimeError.php b/src/Error/RuntimeError.php index 00571fe80..b17bf127b 100644 --- a/src/Error/RuntimeError.php +++ b/src/Error/RuntimeError.php @@ -5,7 +5,6 @@ namespace GoPhp\Error; use GoParser\Ast\Keyword; -use GoParser\Ast\Stmt\IfStmt; use GoParser\Ast\Stmt\Stmt; use GoPhp\Arg; use GoPhp\Argv; @@ -118,7 +117,7 @@ public static function mismatchedTypes(GoType $a, GoType $b): self 'invalid operation: mismatched types %s and %s', $a->name(), $b->name(), - ) + ), ); } @@ -130,14 +129,16 @@ public static function untypedNilInVarDecl(): self public static function nonBooleanCondition(Stmt $context): self { /** @psalm-suppress NoInterfaceProperties */ - $keyword = match (true) { - $context instanceof IfStmt => $context->if, - isset($context->keyword) - && $context->keyword instanceof Keyword => $context->keyword, - default => throw InternalError::unreachable($context), - }; + if (!isset($context->keyword) || !$context->keyword instanceof Keyword) { + throw InternalError::unreachable($context); + } - return new self(sprintf('non-boolean condition in %s statement', $keyword->word)); + return new self( + sprintf( + 'non-boolean condition in %s statement', + $context->keyword->word, + ), + ); } public static function multipleValueInSingleContext(TupleValue $value): self diff --git a/src/ErrorHandler/Collector.php b/src/ErrorHandler/Collector.php index 3e82e4348..0cdc6e770 100644 --- a/src/ErrorHandler/Collector.php +++ b/src/ErrorHandler/Collector.php @@ -7,7 +7,7 @@ use GoPhp\Error\GoError; /** - * Error handler that collects given errors into an array. + * Error handler that collects errors into an array. */ final class Collector implements ErrorHandler { diff --git a/src/ErrorHandler/OutputToStream.php b/src/ErrorHandler/OutputToStream.php index 194676f72..5daac1a04 100644 --- a/src/ErrorHandler/OutputToStream.php +++ b/src/ErrorHandler/OutputToStream.php @@ -8,7 +8,7 @@ use GoPhp\Stream\OutputStream; /** - * Error handler that outputs error messages to a given stream. + * Error handler that outputs error messages to a stream. */ final class OutputToStream implements ErrorHandler { diff --git a/src/GoType/StructType.php b/src/GoType/StructType.php index f8df1ad9a..3e04f3775 100644 --- a/src/GoType/StructType.php +++ b/src/GoType/StructType.php @@ -21,9 +21,11 @@ final class StructType implements GoType { /** * @param array $fields + * @param list $promotedNames */ public function __construct( public readonly array $fields, + public readonly array $promotedNames, ) {} public function name(): string @@ -78,4 +80,9 @@ public function convert(AddressableValue $value): AddressableValue { return DefaultConverter::convert($value, $this); } + + public function hasField(string $name): bool + { + return isset($this->fields[$name]); + } } diff --git a/src/GoValue/Complex/ComplexNumber.php b/src/GoValue/Complex/ComplexNumber.php index d0998ec96..87ae314d7 100644 --- a/src/GoValue/Complex/ComplexNumber.php +++ b/src/GoValue/Complex/ComplexNumber.php @@ -22,9 +22,9 @@ use GoPhp\GoValue\SimpleNumber; use GoPhp\Operator; -use function sprintf; use function GoPhp\assert_values_compatible; use function GoPhp\try_unwind; +use function sprintf; /** * @psalm-type ComplexTuple = array{float, float} @@ -250,7 +250,7 @@ private static function computeForMul(self $lhs, self $rhs): array */ private static function computeForDiv(self $lhs, self $rhs): array { - $denominator = $rhs->real**2 + $rhs->imag**2; + $denominator = $rhs->real ** 2 + $rhs->imag ** 2; $real = ($lhs->real * $rhs->real + $lhs->imag * $rhs->imag) / $denominator; $imag = ($lhs->imag * $rhs->real - $lhs->real * $rhs->imag) / $denominator; diff --git a/src/GoValue/Func/Func.php b/src/GoValue/Func/Func.php index 9c32d43bc..caa411862 100644 --- a/src/GoValue/Func/Func.php +++ b/src/GoValue/Func/Func.php @@ -128,7 +128,7 @@ public function __invoke(Argv $argv): GoValue if ($stmtJump instanceof None) { return $this->type->returnArity === ReturnJump::LEN_VOID - ? new VoidValue() + ? VoidValue::get() : throw RuntimeError::wrongReturnValueNumber([], $this->type->returns); } @@ -165,7 +165,7 @@ public function zeroReturnValue(): GoValue } return match (count($zeroValues)) { - ReturnJump::LEN_VOID => new VoidValue(), + ReturnJump::LEN_VOID => VoidValue::get(), ReturnJump::LEN_SINGLE => $zeroValues[0], default => new TupleValue($zeroValues), }; diff --git a/src/GoValue/Struct/StructValue.php b/src/GoValue/Struct/StructValue.php index 8c7af732e..851fd3e6e 100644 --- a/src/GoValue/Struct/StructValue.php +++ b/src/GoValue/Struct/StructValue.php @@ -102,7 +102,25 @@ public function copy(): self public function accessField(string $name): GoValue { - return $this->fields->get($name)->unwrap(); + $field = $this->fields->tryGet($name); + + if ($field === null) { + foreach ($this->type->promotedNames as $promotedName) { + $field = $this->fields->get($promotedName); + /** @var StructValue $promotedField */ + $promotedField = try_unwind($field->unwrap()); + + if ($promotedField->type->hasField($name)) { + return $promotedField->accessField($name); + } + } + } + + if ($field === null) { + throw RuntimeError::redeclaredName($name); + } + + return $field->unwrap(); } private function equals(self $rhs): BoolValue diff --git a/src/GoValue/VoidValue.php b/src/GoValue/VoidValue.php index 462d3cdef..e50cae067 100644 --- a/src/GoValue/VoidValue.php +++ b/src/GoValue/VoidValue.php @@ -14,6 +14,13 @@ */ final class VoidValue implements GoValue { + private static ?self $instance = null; + + public static function get(): self + { + return self::$instance ??= new self(); + } + public function unwrap(): never { throw RuntimeError::noValueUsedAsValue(); diff --git a/src/Interpreter.php b/src/Interpreter.php index 293a0c8e3..3ed477d3d 100644 --- a/src/Interpreter.php +++ b/src/Interpreter.php @@ -1098,7 +1098,7 @@ private function evalIncDecStmt(IncDecStmt $stmt): None ->evalExpr($stmt->lhs) ->mutate( Operator::fromAst($stmt->op), - new UntypedIntValue(1) + new UntypedIntValue(1), ); return self::$noneJump; diff --git a/src/StmtJump/ReturnJump.php b/src/StmtJump/ReturnJump.php index 66491e486..5be48efca 100644 --- a/src/StmtJump/ReturnJump.php +++ b/src/StmtJump/ReturnJump.php @@ -29,7 +29,7 @@ private function __construct( */ public static function fromVoid(): self { - return new self(new VoidValue(), self::LEN_VOID); + return new self(VoidValue::get(), self::LEN_VOID); } /** diff --git a/src/TypeResolver.php b/src/TypeResolver.php index 29711b669..ac1b8fda7 100644 --- a/src/TypeResolver.php +++ b/src/TypeResolver.php @@ -5,6 +5,7 @@ namespace GoPhp; use Closure; +use GoParser\Ast\EmbeddedFieldDecl; use GoParser\Ast\Expr\ArrayType as AstArrayType; use GoParser\Ast\Expr\Expr; use GoParser\Ast\Expr\FuncType as AstFuncType; @@ -156,18 +157,26 @@ private function resolveStructType(AstStructType $structType, bool $composite): { /** @var array $fields */ $fields = []; + $promotedNames = []; foreach ($structType->fieldDecls as $fieldDecl) { - if ($fieldDecl->identList === null) { - // fixme add anonymous fields - throw InternalError::unimplemented(); - } + $type = $this->resolve($fieldDecl->type, $composite); - if ($fieldDecl->type === null) { - throw InternalError::unimplemented(); - } + if ($fieldDecl instanceof EmbeddedFieldDecl) { + $name = self::getNameForEmbeddedField($fieldDecl); - $type = $this->resolve($fieldDecl->type, $composite); + if (isset($fields[$name])) { + throw RuntimeError::redeclaredName($name); + } + + $fields[$name] = $type; + + if (try_unwind($type) instanceof StructType) { + $promotedNames[] = $name; + } + + continue; + } foreach ($fieldDecl->identList->idents as $ident) { if (isset($fields[$ident->name])) { @@ -178,7 +187,7 @@ private function resolveStructType(AstStructType $structType, bool $composite): } } - return new StructType($fields); + return new StructType($fields, $promotedNames); } private function resolveInterfaceType(AstInterfaceType $interfaceType, bool $composite): InterfaceType @@ -227,4 +236,20 @@ private function getTypeFromEnv(string $name, string $namespace): GoType return $value->unwrap(); } + + private static function getNameForEmbeddedField(EmbeddedFieldDecl $fieldDecl): string + { + $type = $fieldDecl->type; + + if ($type instanceof AstPointerType) { + $type = $type->type; + } + + if ($type instanceof QualifiedTypeName) { + $type = $type->typeName; + } + + /** @var SingleTypeName $type */ + return $type->name->name; + } } diff --git a/tests/Functional/InterpreterTest.php b/tests/Functional/InterpreterTest.php index 7ac4363d6..2f928eb1a 100644 --- a/tests/Functional/InterpreterTest.php +++ b/tests/Functional/InterpreterTest.php @@ -10,6 +10,11 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use function glob; +use function file_get_contents; +use function sprintf; +use function basename; + final class InterpreterTest extends TestCase { private const SRC_FILES_PATH = __DIR__ . '/files'; @@ -43,12 +48,12 @@ public function testSourceFiles(string $goProgram, string $expectedOutput): void public static function sourceFileProvider(): iterable { - $files = \glob(\sprintf('%s/*.go', self::SRC_FILES_PATH)); + $files = glob(sprintf('%s/*.go', self::SRC_FILES_PATH)); foreach ($files as $file) { - $goProgram = \file_get_contents($file); - $expectedOutput = \file_get_contents( - \sprintf('%s/%s.out', self::OUTPUT_FILES_PATH, \basename($file, '.go')) + $goProgram = file_get_contents($file); + $expectedOutput = file_get_contents( + sprintf('%s/%s.out', self::OUTPUT_FILES_PATH, basename($file, '.go')) ); yield $file => [$goProgram, $expectedOutput]; diff --git a/tests/Functional/files/embedded_struct.go b/tests/Functional/files/embedded_struct.go new file mode 100644 index 000000000..f7ba93d77 --- /dev/null +++ b/tests/Functional/files/embedded_struct.go @@ -0,0 +1,150 @@ +package main + +func main() { + test_1() + test_2() + test_3() + test_4() + test_5() + test_6() +} + +func test_1() { + println("test_1") + var a struct{ string } = struct{ string }{} + a.string += "John" + + println(a.string) +} + +func test_2() { + println("test_2") + var a struct { + uint + string + } = struct { + uint + string + }{uint: 100} + + // a.string is taken from stringifyied a.uint + a.string = "Jane" + + println(a.uint) + println(a.string) +} + +func test_3() { + println("test_3") + var a struct { + uint + string + } = struct { + uint + string + }{uint: 100, string: "John"} + + println(a.uint) + println(a.string) +} + +func test_4() { + println("test_4") + type person struct { + string + int + } + + var p1 person = person{string: "John"} + var p2 person = person{string: "Jane", int: 18} + var p3 *person = &p1 + p3.int = 21 + + println(p1.string) + println(p1.int) + println(p2.string) + println(p2.int) + println(p3.string) + println(p3.int) +} + +func test_5() { + println("test_5") + + type name = string + type age int + + type person struct { + name + age + } + + var p1 person = person{name: "John"} + var p2 person = person{name: "Jane", age: 18} + var p3 *person = &p1 + p3.age = 21 + + println(p1.name) + println(p1.age) + println(p2.name) + println(p2.age) + println(p3.name) + println(p3.age) +} + +func test_6() { + println("test_6") + + type name = string + type age int + + type pet struct { + nickname name + age + color string + } + + type person struct { + name + age + pet + } + + var p1 person = person{name: "John"} + p1.nickname = "Fido" + p1.pet.nickname = "Spot" + p1.pet.age = 2 + p1.pet.color = "white" + p1.color = "brown" + + println(p1.name) + println(p1.age) + println(p1.nickname) + println(p1.pet.nickname) + println(p1.pet.age) + println(p1.pet.color) + println(p1.color) + + var p2 person = person{name: "Jane", age: 18, pet: pet{nickname: "Fluffy", age: 2, color: "white"}} + p2.nickname = "Whiskers" + p2.pet.nickname = "Furball" + p2.pet.age = 3 + p2.pet.color = "black" + p2.color = "white" + + println(p2.name) + println(p2.age) + println(p2.nickname) + println(p2.pet.nickname) + println(p2.pet.age) + println(p2.pet.color) + println(p2.color) + + p2.color = "black" + p2.nickname = "Furball" + + println(p2.nickname) + println(p2.color) + println(p2.pet.nickname) + println(p2.pet.color) +} diff --git a/tests/Functional/output/embedded_struct.out b/tests/Functional/output/embedded_struct.out new file mode 100644 index 000000000..ed293d459 --- /dev/null +++ b/tests/Functional/output/embedded_struct.out @@ -0,0 +1,41 @@ +test_1 +John +test_2 +100 +Jane +test_3 +100 +John +test_4 +John +21 +Jane +18 +John +21 +test_5 +John +21 +Jane +18 +John +21 +test_6 +John +0 +Spot +Spot +2 +brown +brown +Jane +18 +Furball +Furball +3 +white +white +Furball +black +Furball +black