src/Values/TypeValue.php

Summary

Maintainability
A
35 mins
Test Coverage
D
67%
<?php

declare(strict_types=1);

namespace Smuuf\Primi\Values;

use \Smuuf\Primi\Context;
use \Smuuf\Primi\Location;
use \Smuuf\Primi\MagicStrings;
use \Smuuf\Primi\Ex\RuntimeError;
use \Smuuf\Primi\Stdlib\BuiltinTypes;
use \Smuuf\Primi\Values\AbstractValue;
use \Smuuf\Primi\Values\AbstractBuiltinValue;
use \Smuuf\Primi\Helpers\Types;
use \Smuuf\Primi\Structures\CallArgs;

/**
 * Instance of this PHP class represents a Primi object which acts as type/class
 * in Primi.
 *
 * Everything in Primi is an object -> types/classes themselves are also
 * objects (instances of some type/class XYZ) -> and that type/class XYZ is
 * represented by this `TypeValue` PHP class.
 *
 * Therefore, the type "type" is an instance of itself.
 */
class TypeValue extends AbstractBuiltinValue {

    public const TYPE = "type";

    /**
     * @param string $name Name of the type.
     * @param ?TypeValue $parent Parent Primi type of this type.
     * @param array<string, AbstractValue> $attrs
     * @param bool $isFinal Final type/class cannot be used as a parent type.
     * @param bool $isMutable IF true, it is possible to mutate type's attrs
     * in userland.
     */
    public function __construct(
        protected string $name,
        protected ?TypeValue $parent = \null,
        protected array $attrs = [],
        protected bool $isFinal = \true,
        protected bool $isMutable = \false,
    ) {

        if ($this->parent?->isFinal()) {
            throw new RuntimeError("Class '{$this->parent->getName()}' cannot be used as a parent class");
        }

    }

    public function getType(): TypeValue {
        return BuiltinTypes::getTypeType();
    }

    public function attrGet(string $name): ?AbstractValue {

        // Try to lookup the attr in this objects attrs. If not found, search
        // the attr in its parent types. And don't bind any found (and then
        // returned) functions to this type object instance.
        // This means "SomeType.some_method()" won't get "SomeType" as
        // first argument - which is what we want - we're accessing the function
        // through a type, not from an instance.
        if ($attr = Types::attr_lookup($this, $name)) {
            return $attr;
        }

        // If the attr was not found in this type object nor its parent types,
        // search through the type of this type object (its metatype/metaclass).
        // If we find a function, we'll bind it to this type object instance -
        // that's because in this case this type object really acts as an
        // instance of its type (metatype).
        return Types::attr_lookup($this->getType(), $name, $this);

    }

    /**
     * Assign an attr to the type instance. Immutable types (those are the
     * basic types that are in the core of Primi) can't be mutated (as they
     * can be shared among different instances of Primi engine within a single
     * PHP runtime). Userland types can be mutable.
     */
    public function attrSet(string $name, AbstractValue $value): bool {

        if (!$this->isMutable) {
            return false;
        }

        $this->attrs[$name] = $value;
        return \true;

    }


    /**
     * Unified method to return a type object any type object *INHERITS FROM*,
     * which is the 'object' object. Easy.
     */
    public function getParentType(): ?TypeValue {
        return $this->parent;
    }

    public function getStringRepr(): string {
        return "<type '{$this->name}'>";
    }

    /**
     * Get name of the type this object represents - as string.
     */
    public function getName(): string {
        return $this->name;
    }

    /**
     * Is this Primi type/class forbidden to be a parent of another type/class?
     */
    public function isFinal(): bool {
        return $this->isFinal;
    }

    public function invoke(
        Context $context,
        ?CallArgs $args = \null,
        ?Location $callsite = \null
    ): ?AbstractValue {

        // Special case: If this type object is _the_ "type" type object.
        if ($this->name === self::TYPE) {
            if ($fn = Types::attr_lookup($this, MagicStrings::MAGICMETHOD_CALL)) {
                return $fn->invoke($context, $args, $callsite);
            }
        }

        // The other possibility: This type object represents other type
        // than the "type" type itself - and calling this object should
        // represent instantiation of new object that will have this type.
        if ($fn = Types::attr_lookup($this, MagicStrings::MAGICMETHOD_NEW, $this)) {

            // The static '__new__()' will receive type object as the first
            // argument.
            $newArgs = $args
                ? (new CallArgs([$this]))->withExtra($args)
                : new CallArgs([$this]);

            $result = $fn->invoke($context, $newArgs, $callsite);

            if ($init = $result->attrGet(MagicStrings::MAGICMETHOD_INIT)) {
                $init->invoke($context, $args);
            }

            return $result;
        }

        return \null;

    }

    public function dirItems(): ?array {

        $fromParents = [];
        $t = $this;
        while ($t = $t->getParentType()) {
            $fromParents = [...$t->dirItems(), ...$fromParents];
        }

        return \array_unique([
            ...$fromParents,
            ...\array_keys($this->attrs),
        ]);

    }

}