diff --git a/src/Builtin/BuiltinFunc/Recover.php b/src/Builtin/BuiltinFunc/Recover.php index 2b9deab52..deba73451 100644 --- a/src/Builtin/BuiltinFunc/Recover.php +++ b/src/Builtin/BuiltinFunc/Recover.php @@ -33,7 +33,7 @@ public function __invoke(Argv $argv): InterfaceValue } // fixme type - return InterfaceValue::nil(new InterfaceType()); + return InterfaceValue::nil(InterfaceType::any()); } public function name(): string diff --git a/src/Builtin/StdBuiltinProvider.php b/src/Builtin/StdBuiltinProvider.php index 7e811479e..4c340904f 100644 --- a/src/Builtin/StdBuiltinProvider.php +++ b/src/Builtin/StdBuiltinProvider.php @@ -19,6 +19,7 @@ use GoPhp\Builtin\BuiltinFunc\Real; use GoPhp\Builtin\BuiltinFunc\Recover; use GoPhp\Env\Environment; +use GoPhp\GoType\InterfaceType; use GoPhp\GoType\NamedType; use GoPhp\GoType\UntypedType; use GoPhp\GoValue\BoolValue; @@ -113,5 +114,6 @@ protected function defineTypes(Environment $env): void $env->defineTypeAlias('byte', $uint8); $env->defineTypeAlias('rune', $int32); + $env->defineTypeAlias('any', new TypeValue(InterfaceType::any())); } } diff --git a/src/Error/InterfaceTypeError.php b/src/Error/InterfaceTypeError.php index bca03de54..bc54247bd 100644 --- a/src/Error/InterfaceTypeError.php +++ b/src/Error/InterfaceTypeError.php @@ -4,37 +4,36 @@ namespace GoPhp\Error; +use GoPhp\GoType\GoType; use GoPhp\GoType\InterfaceType; -use GoPhp\GoType\WrappedType; -use GoPhp\GoValue\WrappedValue; +use GoPhp\GoValue\GoValue; use function sprintf; class InterfaceTypeError extends RuntimeError { - private WrappedValue|WrappedType|null $value = null; + private GoValue|GoType|null $value = null; private ?string $missingMethod = null; - public static function cannotUseAsInterfaceType(WrappedValue|WrappedType $value, InterfaceType $interfaceType): self + public static function cannotUseAsInterfaceType(GoValue|GoType $value, InterfaceType $interfaceType): self { $missingMethod = (string) $interfaceType->tryGetMissingMethod( - $value instanceof WrappedType + $value instanceof GoType ? $value : $value->type(), ); $error = self::cannotUseAsType($value, $interfaceType, $missingMethod); - $error->value = $value; $error->missingMethod = $missingMethod; return $error; } - public static function fromOther(self $error, WrappedType $interfaceType): self + public static function fromOther(self $error, GoType $interfaceType): self { if (!isset($error->value, $error->missingMethod)) { - throw InternalError::unreachable('cannot convert error from other error'); + throw InternalError::unreachable('cannot create interface error from another error'); } return self::cannotUseAsType( @@ -45,21 +44,21 @@ public static function fromOther(self $error, WrappedType $interfaceType): self } private static function cannotUseAsType( - WrappedValue|WrappedType $value, - InterfaceType|WrappedType $interfaceType, + GoValue|GoType $value, + InterfaceType|GoType $interfaceType, string $missingMethod, ): self { return new self( sprintf( - "cannot use %s (value of type %s) as type %s:\n\t%s does not implement %s (missing %s method)", - $value instanceof WrappedType + 'cannot use %s (value of type %s) as type %s: %s does not implement %s (missing method %s)', + $value instanceof GoType ? $value->name() : self::valueToString($value), - $value instanceof WrappedType + $value instanceof GoType ? $value->name() : $value->type()->name(), $interfaceType->name(), - $value instanceof WrappedType + $value instanceof GoType ? $value->name() : $value->type()->name(), $interfaceType->name(), diff --git a/src/Error/InternalError.php b/src/Error/InternalError.php index 7384d98bd..248f69fa1 100644 --- a/src/Error/InternalError.php +++ b/src/Error/InternalError.php @@ -5,6 +5,7 @@ namespace GoPhp\Error; use LogicException; +use ReflectionClass; use function get_debug_type; use function is_object; @@ -14,6 +15,8 @@ /** * Errors that indicate a bug in the code. * They must not occur even when running a wrongly written program. + * + * Non-implemented yet features also throw this exception. */ final class InternalError extends LogicException { @@ -25,7 +28,7 @@ public static function unreachableMethodCall(): self public static function unreachable(object|string|null $context): self { $context = match (true) { - is_object($context) => $context::class, + is_object($context) => (new ReflectionClass($context))->getShortName(), is_string($context) => $context, $context === null => '', }; diff --git a/src/Error/PanicError.php b/src/Error/PanicError.php index 92b199e06..a364373e7 100644 --- a/src/Error/PanicError.php +++ b/src/Error/PanicError.php @@ -4,12 +4,17 @@ namespace GoPhp\Error; +use GoPhp\GoType\GoType; +use GoPhp\GoType\UntypedNilType; use GoPhp\GoValue\AddressableValue; +use GoPhp\GoValue\Interface\InterfaceValue; use GoPhp\GoValue\String\UntypedStringValue; use RuntimeException; use function sprintf; +use const GoPhp\GoValue\NIL; + class PanicError extends RuntimeException implements GoError { public readonly AddressableValue $panicValue; @@ -31,6 +36,27 @@ public static function nilMapAssignment(): self return new self(new UntypedStringValue('assignment to entry in nil map')); } + public static function interfaceConversion(InterfaceValue $interface, GoType $type): self + { + return new self(new UntypedStringValue( + sprintf( + 'interface conversion: %s is %s, not %s', + $interface->type()->name(), + $interface->value === NIL + ? UntypedNilType::NAME + : $interface->value->type()->name(), + $type->name(), + ), + )); + } + + public static function indexOutOfRange(int $index, int $len): self + { + return new self(new UntypedStringValue( + sprintf('index out of range [%d] with length %d', $index, $len), + )); + } + public function getPosition(): null { return null; diff --git a/src/Error/RuntimeError.php b/src/Error/RuntimeError.php index 04568f9f9..b78ac12bc 100644 --- a/src/Error/RuntimeError.php +++ b/src/Error/RuntimeError.php @@ -181,6 +181,11 @@ public static function cannotFindPackage(string $path): self return new self(sprintf('cannot find package "." in: %s', $path)); } + public static function typeOutsideTypeSwitch(): self + { + return new self('invalid syntax tree: use of .(type) outside type switch'); + } + public static function undefinedOperator(Operator $op, AddressableValue $value, bool $unary = false): self { if ($op === Operator::Eq) { @@ -360,6 +365,16 @@ public static function wrongFirstArgumentTypeForAppend(Arg $arg): self ); } + public static function nonInterfaceAssertion(GoValue $value): self + { + return new self( + sprintf( + 'invalid operation: %s is not an interface', + self::valueToString($value), + ), + ); + } + public static function indexNegative(GoValue|int $value): self { return new self( @@ -380,11 +395,6 @@ public static function nonConstantExpr(GoValue $value): self return new self(sprintf('%s is not constant', self::valueToString($value))); } - public static function indexOutOfRange(int $index, int $len): self - { - return new self(sprintf('index out of range [%d] with length %d', $index, $len)); - } - public static function invalidSliceIndices(int $low, int $high): self { return new self(sprintf('invalid slice indices: %d < %d', $low, $high)); diff --git a/src/GoType/InterfaceType.php b/src/GoType/InterfaceType.php index d4c10f5a2..3c6fac647 100644 --- a/src/GoType/InterfaceType.php +++ b/src/GoType/InterfaceType.php @@ -15,18 +15,37 @@ use function sprintf; /** + * @psalm-type MethodMap = array * @template-implements Hashable */ -final class InterfaceType implements Hashable, GoType +final class InterfaceType implements Hashable, RefType { /** - * @param array $methods + * @param MethodMap $methods */ - public function __construct( - private readonly array $methods = [], - private readonly ?Environment $envRef = null, + private function __construct( + private readonly array $methods, + private readonly ?Environment $envRef, ) {} + /** + * @param MethodMap $methods + */ + public static function withMethods(array $methods, Environment $envRef): self + { + return new self($methods, $envRef); + } + + public static function any(): self + { + return new self([], null); + } + + public function isAny(): bool + { + return empty($this->methods); + } + public function name(): string { $methods = []; @@ -43,18 +62,37 @@ public function name(): string public function equals(GoType $other): bool { - return match (true) { - $other instanceof UntypedNilType, - $other instanceof self => true, - $other instanceof WrappedType => $this->checkWrappedType($other), - default => false, - }; + if ($other instanceof UntypedNilType) { + return true; + } + + if ($this->isAny()) { + return true; + } + + if (!$other instanceof self) { + return false; + } + + if ($this->noMissingMethods($other)) { + return true; + } + + return false; } public function isCompatible(GoType $other): bool { - return $other instanceof UntypedNilType - || $this->equals($other); + if ($this->isAny()) { + return true; + } + + return match (true) { + $other instanceof UntypedNilType => true, + $other instanceof WrappedType, + $other instanceof self => $this->noMissingMethods($other), + default => false, + }; } public function zeroValue(): InterfaceValue @@ -68,7 +106,7 @@ public function convert(AddressableValue $value): AddressableValue return $value; } - if ($this->checkWrappedType($value->type())) { + if ($this->noMissingMethods($value->type())) { return $value; } @@ -80,7 +118,7 @@ public function hash(): string return $this->name(); } - public function tryGetMissingMethod(WrappedType $other): ?string + public function tryGetMissingMethod(GoType $other): ?string { if ($this->envRef === null) { return null; @@ -88,22 +126,27 @@ public function tryGetMissingMethod(WrappedType $other): ?string foreach ($this->methods as $name => $method) { if ( - !$this->envRef->hasMethod($name, $other) + $other instanceof WrappedType + && !$this->envRef->hasMethod($name, $other) && !$this->envRef->hasMethod($name, $other->underlyingType) ) { return $name; } + + if ($other instanceof self) { + $hasMethod = isset($other->methods[$name]) && $method->equals($other->methods[$name]); + + if (!$hasMethod) { + return $name; + } + } } return null; } - private function checkWrappedType(WrappedType $other): bool + private function noMissingMethods(self|WrappedType $other): bool { - if ($this->envRef === null) { - return true; - } - return $this->tryGetMissingMethod($other) === null; } } diff --git a/src/GoType/UntypedNilType.php b/src/GoType/UntypedNilType.php index 2f80eced5..c83630953 100644 --- a/src/GoType/UntypedNilType.php +++ b/src/GoType/UntypedNilType.php @@ -12,6 +12,8 @@ */ final class UntypedNilType implements RefType { + public const string NAME = 'nil'; + public function name(): string { return 'untyped nil'; diff --git a/src/GoType/UntypedType.php b/src/GoType/UntypedType.php index f31eb867f..c7c3bfbba 100644 --- a/src/GoType/UntypedType.php +++ b/src/GoType/UntypedType.php @@ -138,12 +138,20 @@ public function isString(): bool return $this === self::UntypedString; } - public function convert(AddressableValue $value): never + public function zeroValue(): AddressableValue { - throw InternalError::unreachableMethodCall(); + return (match ($this) { + self::UntypedInt => NamedType::Int, + self::UntypedRune => NamedType::Int32, + self::UntypedRoundFloat, + self::UntypedFloat => NamedType::Float64, + self::UntypedBool => NamedType::Bool, + self::UntypedComplex => NamedType::Complex128, + self::UntypedString => NamedType::String, + })->zeroValue(); } - public function zeroValue(): never + public function convert(AddressableValue $value): never { throw InternalError::unreachableMethodCall(); } diff --git a/src/GoValue/Float/UntypedFloatValue.php b/src/GoValue/Float/UntypedFloatValue.php index c886bdf5f..1a6b2f7f0 100644 --- a/src/GoValue/Float/UntypedFloatValue.php +++ b/src/GoValue/Float/UntypedFloatValue.php @@ -8,7 +8,7 @@ final class UntypedFloatValue extends FloatNumber { - private UntypedType $type; + private readonly UntypedType $type; public function __construct(float $value) { diff --git a/src/GoValue/Func/FuncValue.php b/src/GoValue/Func/FuncValue.php index 1ee07488d..d8108d684 100644 --- a/src/GoValue/Func/FuncValue.php +++ b/src/GoValue/Func/FuncValue.php @@ -17,24 +17,23 @@ use GoPhp\GoValue\GoValue; use GoPhp\GoValue\PointerValue; use GoPhp\GoValue\RecoverableInvokable; +use GoPhp\GoValue\Ref; use GoPhp\GoValue\SealableTrait; use GoPhp\GoValue\UntypedNilValue; use GoPhp\Operator; -use function spl_object_id; -use function sprintf; use function GoPhp\assert_nil_comparison; use function GoPhp\assert_values_compatible; +use function GoPhp\GoValue\get_address; use const GoPhp\GoValue\NIL; -use const GoPhp\GoValue\ZERO_ADDRESS; /** * @psalm-type FuncCallable = callable(Argv): GoValue * @psalm-import-type FuncBody from Func * @template-implements AddressableValue */ -final class FuncValue implements RecoverableInvokable, AddressableValue +final class FuncValue implements RecoverableInvokable, Ref, AddressableValue { use AddressableTrait; use SealableTrait; @@ -75,9 +74,17 @@ public static function nil(FuncType $type): self return new self(NIL, $type); } + /** + * @psalm-assert !null $this->innerFunc + */ + public function isNil(): bool + { + return $this->innerFunc === NIL; + } + public function zeroReturnValue(): GoValue { - if ($this->innerFunc === NIL) { + if ($this->isNil()) { throw PanicError::nilDereference(); } @@ -86,7 +93,7 @@ public function zeroReturnValue(): GoValue public function bind(AddressableValue $instance): void { - if ($this->innerFunc === NIL) { + if ($this->isNil()) { throw InternalError::unexpectedValue(NIL); } @@ -100,7 +107,7 @@ public function copy(): self public function toString(): string { - return sprintf('0x%x', $this->getAddress()); + return get_address($this); } /** @@ -134,8 +141,8 @@ public function operateOn(Operator $op, GoValue $rhs): BoolValue assert_nil_comparison($this, $rhs, self::NAME); return match ($op) { - Operator::EqEq => new BoolValue($this->innerFunc === NIL), - Operator::NotEq => new BoolValue($this->innerFunc !== NIL), + Operator::EqEq => new BoolValue($this->isNil()), + Operator::NotEq => new BoolValue(!$this->isNil()), default => throw RuntimeError::undefinedOperator($op, $this), }; } @@ -160,13 +167,4 @@ public function mutate(Operator $op, GoValue $rhs): void throw RuntimeError::undefinedOperator($op, $this); } - - private function getAddress(): int - { - if ($this->innerFunc === NIL) { - return ZERO_ADDRESS; - } - - return spl_object_id($this); - } } diff --git a/src/GoValue/Interface/InterfaceValue.php b/src/GoValue/Interface/InterfaceValue.php index 76d63a8f5..4f6613cbd 100644 --- a/src/GoValue/Interface/InterfaceValue.php +++ b/src/GoValue/Interface/InterfaceValue.php @@ -11,32 +11,46 @@ use GoPhp\GoValue\AddressableValue; use GoPhp\GoValue\BoolValue; use GoPhp\GoValue\GoValue; +use GoPhp\GoValue\Ref; +use GoPhp\GoValue\UntypedNilValue; use GoPhp\Operator; -use function GoPhp\assert_nil_comparison; +use function GoPhp\assert_values_compatible; +use function GoPhp\GoValue\get_address; use const GoPhp\GoValue\NIL; /** * @template-implements AddressableValue */ -final class InterfaceValue implements AddressableValue +final class InterfaceValue implements Ref, AddressableValue { use AddressableTrait; public const string NAME = 'interface'; + public readonly InterfaceType $type; + public function __construct( - public readonly ?AddressableValue $value, - // fixme add any - public readonly InterfaceType $type = new InterfaceType(), - ) {} + public ?AddressableValue $value, + ?InterfaceType $type = null, + ) { + $this->type = $type ?? InterfaceType::any(); + } public static function nil(InterfaceType $type): self { return new self(NIL, $type); } + /** + * @psalm-assert !null $this->value + */ + public function isNil(): bool + { + return $this->value === NIL; + } + public function copy(): AddressableValue { return $this; @@ -47,20 +61,63 @@ public function operate(Operator $op): GoValue throw InternalError::unimplemented(); } + /** + * @psalm-suppress all + */ public function operateOn(Operator $op, GoValue $rhs): GoValue { - assert_nil_comparison($this, $rhs, self::NAME); + if ($rhs instanceof UntypedNilValue) { + return match ($op) { + Operator::EqEq => new BoolValue($this->isNil()), + Operator::NotEq => new BoolValue(!$this->isNil()), + default => throw RuntimeError::undefinedOperator($op, $this), + }; + } + + if ($rhs instanceof self) { + assert_values_compatible($this, $rhs); + + if ($this->isNil() || $rhs->isNil()) { + return match ($op) { + Operator::EqEq => new BoolValue($this->isNil() && $rhs->isNil()), + Operator::NotEq => new BoolValue(!$this->isNil() || !$rhs->isNil()), + default => throw RuntimeError::undefinedOperator($op, $this), + }; + } + + return match ($op) { + Operator::EqEq => $this->value->operateOn(Operator::EqEq, $rhs->value), + Operator::NotEq => $this->value->operateOn(Operator::NotEq, $rhs->value), + default => throw RuntimeError::undefinedOperator($op, $this), + }; + } + + assert_values_compatible($this->value, $rhs); return match ($op) { - Operator::EqEq => new BoolValue($this->value === NIL), - Operator::NotEq => new BoolValue($this->value !== NIL), + Operator::EqEq => $this->value->operateOn(Operator::EqEq, $rhs), + Operator::NotEq => $this->value->operateOn(Operator::NotEq, $rhs), default => throw RuntimeError::undefinedOperator($op, $this), }; } public function mutate(Operator $op, GoValue $rhs): void { - throw InternalError::unimplemented(); + if ($op === Operator::Eq) { + if ($rhs instanceof UntypedNilValue) { + $this->value = NIL; + + return; + } + + assert_values_compatible($this, $rhs); + + $this->value = $rhs; + + return; + } + + throw RuntimeError::undefinedOperator($op, $this); } public function unwrap(): mixed @@ -75,7 +132,6 @@ public function type(): InterfaceType public function toString(): string { - // fixme implement - return $this->value?->toString() ?? 'nil'; + return get_address($this); } } diff --git a/src/GoValue/Map/MapValue.php b/src/GoValue/Map/MapValue.php index 34145db62..00f330b89 100644 --- a/src/GoValue/Map/MapValue.php +++ b/src/GoValue/Map/MapValue.php @@ -14,6 +14,7 @@ use GoPhp\GoValue\BoolValue; use GoPhp\GoValue\GoValue; use GoPhp\GoValue\PointerValue; +use GoPhp\GoValue\Ref; use GoPhp\GoValue\UntypedNilValue; use GoPhp\Operator; @@ -36,7 +37,7 @@ * psalm bug with Intersection types in generics * @psalm-suppress PossiblyUndefinedMethod */ -final class MapValue implements Map, AddressableValue +final class MapValue implements Map, Ref, AddressableValue { use AddressableTrait; @@ -57,6 +58,14 @@ public static function nil(MapType $type): self return new self(NIL, $type); } + /** + * @psalm-assert !null $this->innerMap + */ + public function isNil(): bool + { + return $this->innerMap === NIL; + } + public function toString(): string { $str = []; @@ -96,7 +105,7 @@ function () use ($defaultValue, $at): void { public function set(GoValue $value, GoValue $at): void { - if ($this->innerMap === NIL) { + if ($this->isNil()) { throw PanicError::nilMapAssignment(); } @@ -142,8 +151,8 @@ public function operateOn(Operator $op, GoValue $rhs): BoolValue assert_nil_comparison($this, $rhs, self::NAME); return match ($op) { - Operator::EqEq => new BoolValue($this->innerMap === NIL), - Operator::NotEq => new BoolValue($this->innerMap !== NIL), + Operator::EqEq => new BoolValue($this->isNil()), + Operator::NotEq => new BoolValue(!$this->isNil()), default => throw RuntimeError::undefinedOperator($op, $this), }; } diff --git a/src/GoValue/PointerValue.php b/src/GoValue/PointerValue.php index 6eb229bf1..53dfd7178 100644 --- a/src/GoValue/PointerValue.php +++ b/src/GoValue/PointerValue.php @@ -4,20 +4,19 @@ namespace GoPhp\GoValue; +use GoPhp\Error\InternalError; use GoPhp\Error\RuntimeError; use GoPhp\Error\PanicError; use GoPhp\GoType\PointerType; use GoPhp\Operator; -use function spl_object_id; -use function sprintf; use function GoPhp\assert_values_compatible; /** * @psalm-type Address = int * @template-implements AddressableValue
*/ -final class PointerValue implements AddressableValue +final class PointerValue implements Ref, AddressableValue { use AddressableTrait; @@ -36,18 +35,26 @@ public static function nil(PointerType $type): self return new self(NIL, $type); } + /** + * @psalm-assert !null $this->pointsTo + */ + public function isNil(): bool + { + return $this->pointsTo === NIL; + } + public function getPointsTo(): AddressableValue { - if ($this->pointsTo === NIL) { + if ($this->isNil()) { throw PanicError::nilDereference(); } return $this->pointsTo; } - public function unwrap(): int + public function unwrap(): string { - return $this->getAddress(); + throw InternalError::unreachable($this); } public function operate(Operator $op): AddressableValue @@ -81,6 +88,7 @@ public function mutate(Operator $op, GoValue $rhs): void return; } + /** @var self $rhs */ $this->pointsTo = $rhs->pointsTo; return; @@ -101,22 +109,13 @@ public function type(): PointerType public function toString(): string { - return sprintf('0x%x', $this->getAddress()); - } - - private function getAddress(): int - { - if ($this->pointsTo === NIL) { - return ZERO_ADDRESS; - } - - return spl_object_id($this->pointsTo); + return get_address($this); } private function equals(self|UntypedNilValue $rhs): BoolValue { if ($rhs instanceof UntypedNilValue) { - return new BoolValue($this->pointsTo === NIL); + return new BoolValue($this->isNil()); } return new BoolValue($rhs->pointsTo === $this->pointsTo); diff --git a/src/GoValue/Ref.php b/src/GoValue/Ref.php new file mode 100644 index 000000000..e782d737f --- /dev/null +++ b/src/GoValue/Ref.php @@ -0,0 +1,10 @@ + * @template-implements AddressableValue> */ -final class SliceValue implements Sliceable, Unpackable, Sequence, AddressableValue +final class SliceValue implements Sliceable, Unpackable, Sequence, Ref, AddressableValue { use AddressableTrait; @@ -81,6 +82,14 @@ public static function nil(SliceType $type): self return new self(NIL, $type); } + /** + * @psalm-assert !null $this->values + */ + public function isNil(): bool + { + return $this->values === NIL; + } + /** * @template T of AddressableValue * @@ -159,7 +168,7 @@ public function unpack(): iterable */ public function append(GoValue $value): void { - if ($this->values === NIL) { + if ($this->isNil()) { $this->values = UnderlyingArray::fromEmpty(); } @@ -184,8 +193,8 @@ public function operateOn(Operator $op, GoValue $rhs): BoolValue assert_nil_comparison($this, $rhs, self::NAME); return match ($op) { - Operator::EqEq => new BoolValue($this->values === NIL), - Operator::NotEq => new BoolValue($this->values !== NIL), + Operator::EqEq => new BoolValue($this->isNil()), + Operator::NotEq => new BoolValue(!$this->isNil()), default => throw RuntimeError::undefinedOperator($op, $this), }; } @@ -243,7 +252,7 @@ public function clone(): self */ public function copyFromSequence(Sequence $seq): int { - if ($this->values === NIL) { + if ($this->isNil()) { return 0; } diff --git a/src/GoValue/utils.php b/src/GoValue/utils.php index 19d3f452b..d50a97aab 100644 --- a/src/GoValue/utils.php +++ b/src/GoValue/utils.php @@ -4,6 +4,9 @@ namespace GoPhp\GoValue; +use function spl_object_id; +use function dechex; + /** * Alias for `null` in place of `nil` value in reference types. * @@ -17,3 +20,17 @@ * @internal */ const ZERO_ADDRESS = 0x0; + +/** + * Get the address of an object. + * + * @internal + */ +function get_address(Ref $refValue): string +{ + $address = $refValue->isNil() + ? ZERO_ADDRESS + : spl_object_id($refValue); + + return '0x' . dechex($address); +} diff --git a/src/ImportHandler.php b/src/ImportHandler.php index 4d1c570d0..9822dac8c 100644 --- a/src/ImportHandler.php +++ b/src/ImportHandler.php @@ -14,8 +14,8 @@ class ImportHandler { - final protected const string EXTENSION_GO = '.go'; - final protected const string EXTENSION_GO_PHP = '.gop'; + final protected const string EXTENSION_GO = 'go'; + final protected const string EXTENSION_GO_PHP = 'gop'; protected const array EXTENSIONS = [ self::EXTENSION_GO, @@ -50,7 +50,7 @@ public function importFromPath(string $path): iterable } foreach ($this->extensions as $extension) { - $file = $path . $extension; + $file = sprintf('%s.%s', $path, $extension); if (is_file($file)) { yield $this->importFromFile($file); diff --git a/src/Interpreter.php b/src/Interpreter.php index b9fb788bd..b90377b83 100644 --- a/src/Interpreter.php +++ b/src/Interpreter.php @@ -24,6 +24,7 @@ use GoParser\Ast\Expr\SimpleSliceExpr; use GoParser\Ast\Expr\StringLit; use GoParser\Ast\Expr\Type as AstType; +use GoParser\Ast\Expr\TypeAssertionExpr; use GoParser\Ast\Expr\UnaryExpr; use GoParser\Ast\ExprCaseClause; use GoParser\Ast\ExprList; @@ -95,6 +96,7 @@ use GoPhp\GoValue\GoValue; use GoPhp\GoValue\Int\UntypedIntValue; use GoPhp\GoValue\Interface\InterfaceBuilder; +use GoPhp\GoValue\Interface\InterfaceValue; use GoPhp\GoValue\Invokable; use GoPhp\GoValue\Map\MapBuilder; use GoPhp\GoValue\Map\MapLookupValue; @@ -749,6 +751,22 @@ private function evalSliceExpr(SimpleSliceExpr|FullSliceExpr $expr): GoValue&Sli return $sequence->slice($low, $high, $max); } + private function evalTypeAssertionExpr(TypeAssertionExpr $expr): GoValue + { + $value = $this->evalExpr($expr->expr); + $type = $this->typeResolver->resolve($expr->type); + + if (!$value instanceof InterfaceValue) { + throw RuntimeError::nonInterfaceAssertion($value); + } + + if ($value->isNil() || !$type->isCompatible($value->value->type())) { + throw PanicError::interfaceConversion($value, $type); + } + + return $value->value; + } + private function getSliceExprIndex(?Expr $expr): ?int { if ($expr === null) { @@ -1237,6 +1255,9 @@ private function evalShortVarDeclStmt(ShortVarDecl $stmt): None return self::$noneJump; } + /** + * @return array + */ private function collectValuesFromExprList(ExprList $exprList, int $expectedLen): array { $value = $this->evalExpr($exprList->exprs[0]); @@ -1294,6 +1315,7 @@ private function evalExpr(Expr $expr): GoValue $expr instanceof IndexExpr => $this->evalIndexExpr($expr), $expr instanceof SimpleSliceExpr => $this->evalSliceExpr($expr), $expr instanceof FullSliceExpr => $this->evalSliceExpr($expr), + $expr instanceof TypeAssertionExpr => $this->evalTypeAssertionExpr($expr), $expr instanceof CompositeLit => $this->evalCompositeLit($expr), $expr instanceof FuncLit => $this->evalFuncLit($expr), default => throw InternalError::unreachable($expr), @@ -1551,17 +1573,21 @@ private static function isTrue(GoValue $value, Stmt $context): bool private function defineVar(string $name, GoValue $value, ?GoType $type = null): void { $this->checkNonDeclarableNames($name); + $initValue = null; if ($value instanceof UntypedNilValue && $type instanceof RefType) { - $value = $type->zeroValue(); + $initValue = $type->zeroValue(); + } elseif ($type instanceof InterfaceType) { + $initValue = $type->zeroValue(); + $initValue->mutate(Operator::Eq, $value); } else { /** @var AddressableValue $value */ - $value = $value->copy(); + $initValue = $value->copy(); } $this->env->defineVar( $name, - $value, + $initValue, reify_untyped($type ?? $value->type()), $this->scopeResolver->resolveDefinitionScope(), ); diff --git a/src/TypeResolver.php b/src/TypeResolver.php index 843d48e5b..45a41bdd5 100644 --- a/src/TypeResolver.php +++ b/src/TypeResolver.php @@ -11,6 +11,7 @@ use GoParser\Ast\Expr\FuncType as AstFuncType; use GoParser\Ast\Expr\InterfaceType as AstInterfaceType; use GoParser\Ast\Expr\MapType as AstMapType; +use GoParser\Ast\Expr\ParenType; use GoParser\Ast\Expr\PointerType as AstPointerType; use GoParser\Ast\Expr\QualifiedTypeName; use GoParser\Ast\Expr\SingleTypeName; @@ -18,6 +19,7 @@ use GoParser\Ast\Expr\StructType as AstStructType; use GoParser\Ast\Expr\Type as AstType; use GoParser\Ast\Expr\TypeTerm; +use GoParser\Ast\Keyword; use GoParser\Ast\MethodElem; use GoParser\Ast\Params as AstParams; use GoParser\Ast\Signature as AstSignature; @@ -55,6 +57,7 @@ public function resolve(AstType $type, bool $composite = false): GoType return match (true) { $type instanceof SingleTypeName => $this->resolveTypeFromSingleName($type), $type instanceof QualifiedTypeName => $this->resolveTypeFromQualifiedName($type), + $type instanceof ParenType => $this->resolveTypeFromParenType($type), $type instanceof AstFuncType => $this->resolveTypeFromAstSignature($type->signature), $type instanceof AstArrayType => $this->resolveArrayType($type, $composite), $type instanceof AstSliceType => $this->resolveSliceType($type, $composite), @@ -112,6 +115,15 @@ private function resolveTypeFromQualifiedName(QualifiedTypeName $type): GoType ); } + private function resolveTypeFromParenType(ParenType $type): GoType + { + if ($type->type instanceof Keyword) { + throw RuntimeError::typeOutsideTypeSwitch(); + } + + return $this->resolve($type->type); + } + private function resolveArrayType(AstArrayType $arrayType, bool $composite): ArrayType { $elemType = $this->resolve($arrayType->elemType, $composite); @@ -210,7 +222,7 @@ private function resolveInterfaceType(AstInterfaceType $interfaceType, bool $com } } - return new InterfaceType($methods, $this->envRef); + return InterfaceType::withMethods($methods, $this->envRef); } /** diff --git a/src/asserts.php b/src/asserts.php index e90c932e8..6ace59770 100644 --- a/src/asserts.php +++ b/src/asserts.php @@ -7,12 +7,12 @@ use GoPhp\Builtin\BuiltinFunc\BuiltinFunc; use GoPhp\Error\InterfaceTypeError; use GoPhp\Error\InternalError; +use GoPhp\Error\PanicError; use GoPhp\Error\RuntimeError; use GoPhp\GoType\GoType; use GoPhp\GoType\InterfaceType; use GoPhp\GoType\NamedType; use GoPhp\GoType\UntypedType; -use GoPhp\GoType\WrappedType; use GoPhp\GoValue\AddressableValue; use GoPhp\GoValue\Castable; use GoPhp\GoValue\Float\FloatNumber; @@ -40,7 +40,7 @@ function assert_types_compatible(GoType $a, GoType $b): void return; } - if ($a instanceof InterfaceType && $b instanceof WrappedType) { + if ($a instanceof InterfaceType) { throw InterfaceTypeError::cannotUseAsInterfaceType($b, $a); } @@ -62,7 +62,7 @@ function assert_types_equal(GoType $a, GoType $b): void return; } - if ($a instanceof InterfaceType && $b instanceof WrappedType) { + if ($a instanceof InterfaceType) { throw InterfaceTypeError::cannotUseAsInterfaceType($b, $a); } @@ -199,7 +199,7 @@ function assert_index_exists(int $index, int $max): void assert_index_positive($index); if ($index >= $max) { - throw RuntimeError::indexOutOfRange($index, $max); + throw PanicError::indexOutOfRange($index, $max); } } diff --git a/tests/Functional/files/builtin.go b/tests/Functional/files/builtin.go index cf25b2b84..0dfbc060f 100644 --- a/tests/Functional/files/builtin.go +++ b/tests/Functional/files/builtin.go @@ -53,3 +53,5 @@ func test_append() { s3 = append(s3, "str"...) println(len(s3), cap(s3), s3[0], s3[1], s3[2], s3[3], s3[4], s3[5], s3[6], s3[7]) } + +// todo diff --git a/tests/Functional/files/interface_any.go b/tests/Functional/files/interface_any.go new file mode 100644 index 000000000..f5a3d7b39 --- /dev/null +++ b/tests/Functional/files/interface_any.go @@ -0,0 +1,169 @@ +package main + +func main() { + test_interfaceAny1() + test_interfaceAny2() + test_interfaceAny3() + test_interfaceAny4() +} + +// any value assignment +func test_interfaceAny1() { + println("test_interfaceAny1") + + var a interface{} + + a = 1 + + println(a == nil) + println(a == 1) + + a = false + + println(a == nil) + println(a == false) + + a = "string" + + println(a == nil) + println(a == "string") + + a = []int{1, 2, 3} + + println(a == nil) + + a = map[string]int{"a": 1} + + println(a == nil) + + a = nil + + println(a == nil) +} + +// value assigment after variable declaration with value +func test_interfaceAny2() { + println("test_interfaceAny2") + + var a interface{} = nil + + a = "hello" + + println(a == nil) + println(a == "hello") + + var b any = 4 + + b = "hello" + + println(b == nil) + println(b == "hello") +} + +// var declaration with value +func test_interfaceAny3() { + println("test_interfaceAny3") + + var a interface{} = []int{1, 2, 3} + + println(a == nil) + + var b any = [1]bool{true} + + println(b == nil) + + var c any = [...]struct{ Name string }{struct{ Name string }{Name: "John"}} + + println(c == nil) + + var d any = map[string]int{"a": 1} + + println(d == nil) + + var e any = nil + + println(e == nil) + + var f any = 1 + + println(f == nil) + println(f == 1) + + var g any = "hello" + + println(g == nil) + println(g == "hello") + + var h any = false + + println(h == nil) + println(h == false) + + var i any = 1.2 + + println(i == nil) + println(i == 1.2) + + var j any = 1.2 + 3.4i + + println(j == nil) + println(j == 1.2+3.4i) + + var k any = 'a' + + println(k == nil) + println(k == 'a') + + var l any = struct{ Name string }{Name: "John"} + + println(l == nil) +} + +// struct type assignment +func test_interfaceAny4() { + println("test_interfaceAny4") + + type Person struct { + Name string + } + + var a interface{} = Person{Name: "John"} + + println(a == nil) + + a = &Person{Name: "John"} + + println(a == nil) + + a = "hello" + + println(a == nil) + + var b bool = true + + a = b + + println(a == nil) + println(a == true) + println(a == b) + + var c any = 18.3 + + a = c + + println(a == nil) + println(a == 18.3) + println(a == c) + + var d []int = []int{1, 2, 3} + + a = d + + println(a == nil) + + var e map[string]int + + a = e + + println(a == nil) +} diff --git a/tests/Functional/files/recover.go b/tests/Functional/files/recover.go index d0bb17ddd..e2b02d479 100644 --- a/tests/Functional/files/recover.go +++ b/tests/Functional/files/recover.go @@ -29,7 +29,7 @@ func f() (int, bool) { defer func() { if r := recover(); r != nil { println("Recovered in f") - println(r) + println(r == nil) } }() println("Calling g.") @@ -67,7 +67,7 @@ func customPanic(msg string) { func customRecoverFromPanic(msg string) int { var x, y = recover(), recover() - println(x) + println(x == nil) println(y) println(msg) @@ -101,7 +101,7 @@ func checkAndGet(a []int, index int) (int, bool) { func handleOutOfBounds() { if r := recover(); r != nil { - println("Recovering from panic:", r) + println("Recovering from panic:", r == nil) } } @@ -119,7 +119,7 @@ func test_5() { func handleNilDereference() { if r := recover(); r != nil { - println("Recovering from panic:", r) + println("Recovering from panic:", r == nil) } else { println("No panic") } @@ -142,7 +142,7 @@ func test_6() { func handleNilMapAssignment() { if r := recover(); r != nil { - println("Recovering from panic:", r) + println("Recovering from panic:", r == nil) } else { println("No panic") } diff --git a/tests/Functional/output/interface_any.out b/tests/Functional/output/interface_any.out new file mode 100644 index 000000000..f85d67fee --- /dev/null +++ b/tests/Functional/output/interface_any.out @@ -0,0 +1,46 @@ +test_interfaceAny1 +false +true +false +true +false +true +false +false +true +test_interfaceAny2 +false +true +false +true +test_interfaceAny3 +false +false +false +false +true +false +true +false +true +false +true +false +true +false +true +false +true +false +test_interfaceAny4 +false +false +false +false +true +true +false +true +true +false +false diff --git a/tests/Functional/output/recover.out b/tests/Functional/output/recover.out index 26dcf0d3a..593e27dcb 100644 --- a/tests/Functional/output/recover.out +++ b/tests/Functional/output/recover.out @@ -1,5 +1,5 @@ test_1 -nil +0x0 Returned normally from test_1. test_2 Calling g. @@ -13,26 +13,26 @@ Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f -4 +false Returned normally from f. 0 false test_3 -panicking from test_3 -nil +false +0x0 second call to recover from test_3 -nil -nil +true +0x0 recovering from test_3 test_4 -Recovering from panic: Out of bound access for slice +Recovering from panic: false Val: 0 Error: false Val: 6 Error: true test_5 -Recovering from panic: runtime error: invalid memory address or nil pointer dereference +Recovering from panic: false No panic test_6 -Recovering from panic: assignment to entry in nil map +Recovering from panic: false No panic No panic