src/Values/AbstractValue.php
<?php
declare(strict_types=1);
namespace Smuuf\Primi\Values;
use \Smuuf\Primi\Context;
use \Smuuf\Primi\Location;
use \Smuuf\Primi\Ex\TypeError;
use \Smuuf\Primi\Ex\EngineError;
use \Smuuf\Primi\Ex\UnhashableTypeException;
use \Smuuf\Primi\Values\TypeValue;
use \Smuuf\Primi\Helpers\Func;
use \Smuuf\Primi\Helpers\Types;
use \Smuuf\Primi\Helpers\Interned;
use \Smuuf\Primi\Helpers\ValueFriends;
use \Smuuf\Primi\Structures\CallArgs;
use \Smuuf\Primi\Structures\FnContainer;
/**
* Primi value == Primi object in our case.
*/
abstract class AbstractValue extends ValueFriends {
/** @const string Name of Primi (object) type. */
public const TYPE = '__undefined__';
/**
* Take any PHP value and convert it into a Primi value object of an
* appropriate type.
*
* NOTE: We're not checking \is_callable on bare $value, because for example
* string 'time' would be determined to be the PHP's 'time' function and
* we do not want that (and it would also be a security issue).
*
* @param mixed $value
* @return AbstractValue
*/
public static function buildAuto($value) {
switch (\true) {
case $value instanceof AbstractValue:
return $value;
case $value === \null:
return Interned::null();
case \is_bool($value):
return Interned::bool($value);
case \is_int($value) || \is_float($value) || \is_numeric($value):
return Interned::number(Func::scientific_to_decimal((string) $value));
case \is_string($value):
return Interned::string($value);
case \is_array($value):
if (\is_callable($value)) {
return new FuncValue(FnContainer::buildFromClosure($value));
}
$inner = \array_map(__METHOD__, $value);
if (!\array_is_list($value)) {
return new DictValue(Func::array_to_couples($inner));
}
return new ListValue($inner);
case $value instanceof \Closure;
return new FuncValue(FnContainer::buildFromClosure($value));
}
// Otherwise throw an error.
throw new EngineError(\sprintf(
"Unable to auto-build Primi object from '%s'",
\get_debug_type($value)
));
}
/**
* Returns the core PHP value of this Primi value object.
*
* @return mixed
*/
final public function getCoreValue() {
return $this->value;
}
/**
* Returns an unambiguous string representation of internal value.
*
* If possible, is should be in such form that it the result of this
* method can be used as Primi source code to recreate that value.
*/
abstract public function getStringRepr(): string;
/**
* Returns a string representation of value.
*/
public function getStringValue(): string {
return $this->getStringRepr();
}
/**
* Returns dict array with this all attributes of this value.
*
* @return array<string, AbstractValue>
*/
final public function getAttrs(): array {
return $this->attrs;
}
/**
* Return the Primi type object this Primi value is instance of.
*/
abstract public function getType(): TypeValue;
/**
* Return the name of Primi type of this value as string.
*/
public function getTypeName(): string {
return $this->getType()->getName();
}
// Length.
/**
* Values can report the length of it (i.e. its internal value).
* Values without any meaningful length can report null (default).
*/
public function getLength(): ?int {
return \null;
}
//
// Truthiness.
//
/**
* All values must be able to tell if they're truthy or falsey.
* All values are truthy unless they tell otherwise.
*/
public function isTruthy(): bool {
return \true;
}
//
// Comparisons - Equality.
//
/**
* All values support comparison.
*
* Default implementation below says that two values are equal if they're
* the same PHP object.
*/
public function isEqualTo(
AbstractValue $right
): ?bool {
return $this === $right;
}
//
// Comparisons - Relation.
//
/**
* If a value knows how to evaluate relation to other values, it shall
* define that by overriding this default logic. (By default a value does
* not know anything about its relation of itself to other values.)
*
* Relation in this scope means the result of <, >, <=, >= operations.
*/
public function hasRelationTo(string $operator, AbstractValue $right): ?bool {
return \null;
}
public function doesContain(AbstractValue $right): ?bool {
return \null;
}
public function doAddition(AbstractValue $right): ?AbstractValue {
return \null;
}
public function doSubtraction(AbstractValue $right): ?AbstractValue {
return \null;
}
public function doMultiplication(AbstractValue $right): ?AbstractValue {
return \null;
}
public function doDivision(AbstractValue $right): ?AbstractValue {
return \null;
}
public function doPower(AbstractValue $right): ?AbstractValue {
return \null;
}
/**
* Called when used as `some_value()`.
*
* If the value is not callable, throws TypeError.
*
* This API differs from "return null when unsupported" used elsewhere,
* because then someone would always have to check if the returned value
* was a PHP null, to find out the value does not support invocation. Every
* time, after each invocation (which doesn't even make much sense).
*
* This way the exception is just simply thrown once.
*
* @param Context $context Runtime context of the call-site.
* @param ?CallArgs $args Args object with call arguments (optional).
* @param ?Location $callsite Call site location (optional).
* @throws TypeError
*/
public function invoke(
Context $context,
?CallArgs $args = \null,
?Location $callsite = \null
): ?AbstractValue {
throw new TypeError("'{$this->getTypeName()}' object is not callable");
}
/**
* @return ?\Iterator<int, AbstractValue>
*/
public function getIterator(): ?\Iterator {
return \null;
}
/**
* Assign a value under specified key into this value.
*
* Must return `true` on successful assignment, or `false` if assignment is
* not supported.
*/
public function itemSet(?AbstractValue $key, AbstractValue $value): bool {
return \false;
}
/**
* Returns some internal value by specified key.
*
* Must return some value object, or `null` if such operation is not
* supported.
*/
public function itemGet(AbstractValue $key): ?AbstractValue {
return \null;
}
/**
* Assign an attr to the value.
*
* Must return true on successful assignment, or `false` if assignment is
* not supported.
*
* NOTE: This attribute name can only be strings, so there's no need to
* accept StringValue as $key.
*/
public function attrSet(string $name, AbstractValue $value): bool {
return \false;
}
/**
* Returns an attr from the value.
*
* This must return either a value object (which is an attribute of this
* value object) or `null`, if not found.
*
* If this returns `null`, object hierarchy will be traversed upwards and
* attr will be searched in the parent object.
*
* This API is differs from, for example, `self::itemGet()`, as `null` does
* NOT represent an "unsupported" operation, but rather "it's not here, try
* elsewhere".
*
* As the above is expected to be the most common thing to do, unsupported
* attr access instead should throw a RuntimeError.
*
* NOTE: This attribute name can only be strings, so there's no need to
* accept StringValue as $key.
*/
public function attrGet(string $name): ?AbstractValue {
return $this->attrs[$name]
?? Types::attr_lookup($this->getType(), $name, $this);
}
/**
* Return attribute from this Primi object - without any inheritance or
* type shenanigans.
*
* For internal usage by Primi engine - keeping the $attr property of
* PHP AbstractValue class protected.
*
* @internal
*/
final public function rawAttrGet(string $name): ?AbstractValue {
return $this->attrs[$name] ?? \null;
}
/**
* Returns a scalar value to be used as a hash value that can be used as
* scalar key for PHP arrays used in Primi internals.
*
* @throws UnhashableTypeException
*/
public function hash(): string {
throw new UnhashableTypeException($this->getTypeName());
}
/**
* Return PHP array contain listing of items/attrs inside the object.
*
* This is mainly for the builtin dir() function Primi provides for
* easy inspection of contents of an object.
*
* @return array<string>
*/
public function dirItems(): ?array {
$fromParents = $this->getType()->dirItems();
$t = $this->getType();
while ($t = $t->getParentType()) {
$fromParents = [...$t->dirItems(), ...$fromParents];
}
return \array_unique([
...$fromParents,
...\array_keys($this->attrs),
]);
}
}