src/Stdlib/Modules/std/__builtins__.primi.php

Summary

Maintainability
A
2 hrs
Test Coverage
B
81%
<?php

declare(strict_types=1);

namespace Smuuf\Primi\Stdlib\Modules;

use \Smuuf\Primi\Repl;
use \Smuuf\Primi\Context;
use \Smuuf\Primi\Extensions\PrimiFunc;
use \Smuuf\Primi\Ex\LookupError;
use \Smuuf\Primi\Ex\RuntimeError;
use \Smuuf\Primi\Helpers\Func;
use \Smuuf\Primi\Values\NullValue;
use \Smuuf\Primi\Values\BoolValue;
use \Smuuf\Primi\Values\TupleValue;
use \Smuuf\Primi\Values\NumberValue;
use \Smuuf\Primi\Values\StringValue;
use \Smuuf\Primi\Values\AbstractValue;
use \Smuuf\Primi\Values\IteratorFactoryValue;
use \Smuuf\Primi\Values\ListValue;
use \Smuuf\Primi\Helpers\Interned;
use \Smuuf\Primi\Modules\NativeModule;
use \Smuuf\Primi\Structures\CallArgs;
use \Smuuf\Primi\Modules\AllowedInSandboxTrait;

return new
/**
 * Internal module where _built-ins_ are stored.
 *
 * This module doesn't need to be imported - its contents are available by
 * default in every scope.
 */
class extends NativeModule {

    use AllowedInSandboxTrait;

    public function execute(Context $ctx): array {

        $types = $ctx->getImporter()
            ->getModule('std.types')
            ->getCoreValue();

        return [
            'object' => $types->getVariable('object'),
            'type' => $types->getVariable('type'),
            'bool' => $types->getVariable('bool'),
            'dict' => $types->getVariable('dict'),
            'list' => $types->getVariable('list'),
            'tuple' => $types->getVariable('tuple'),
            'number' => $types->getVariable('number'),
            'regex' => $types->getVariable('regex'),
            'string' => $types->getVariable('string'),
            'NotImplemented' => Interned::constNotImplemented(),
        ];

    }

    /**
     * _**Only in [CLI](https://w.wiki/QPE)**_.
     *
     * Prints value to standard output.
     */
    #[PrimiFunc(callConv: PrimiFunc::CONV_CALLARGS)]
    public static function print(
        CallArgs $args,
        Context $ctx
    ): NullValue {

        $io = $ctx->getStdIoDriver();

        if ($args->isEmpty()) {
            $io->stdout("\n");
            return Interned::null();
        }

        $args = $args->extract(['*args', 'end', 'sep'], ['end', 'sep']);
        $end = $args['end'] ?? Interned::string("\n");
        $sep = $args['sep'] ?? Interned::string(" ");

        $pieces = \array_map(
            static fn($v) => $v->getStringValue(),
            $args['args']->getCoreValue()
        );

        $io->stdout(
            \implode($sep->getStringValue(), $pieces),
            $end->getStringValue()
        );

        return Interned::null();

    }

    /**
     * _**Only in [CLI](https://w.wiki/QPE)**_.
     *
     * Injects a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
     * session for debugging at the specified line.
     */
    #[PrimiFunc(callConv: PrimiFunc::CONV_CALLARGS)]
    public static function debugger(
        CallArgs $args,
        Context $ctx
    ): AbstractValue {

        $args->extractPositional(0);
        if ($ctx->getConfig()->getSandboxMode()) {
            throw new RuntimeError("Function 'debugger' disabled in sandbox");
        }

        $repl = new Repl('debugger');
        $repl->start($ctx);

        return Interned::null();

    }


    /**
     * Returns length of a value.
     *
     * ```js
     * len("hello, Česká Třebová") == 20
     * len(123456) == 6
     * len([1, 2, 3]) == 3
     * len({'a': 1, 'b': 'c'}) == 2
     * ```
     */
    #[PrimiFunc (toStack: \false)]
    public static function len(AbstractValue $value): NumberValue {

        $length = $value->getLength();
        if ($length === \null) {
            $type = $value->getTypeName();
            throw new RuntimeError("Type '$type' does not support length");
        }

        return Interned::number((string) $value->getLength());

    }

    /**
     * This function returns `true` if a `bool` value passed into it is `true`
     * and throws error if it's `false`. Optional `string` description can be
     * provided, which will be visible in the eventual error message.
     */
    #[PrimiFunc]
    public static function assert(
        BoolValue $assumption,
        ?StringValue $description = \null
    ): BoolValue {

        $desc = $description;
        if ($assumption->value !== \true) {
            $desc = ($desc && $desc->value !== '') ? " ($desc->value)" : '';
            throw new RuntimeError(\sprintf("Assertion failed%s", $desc));
        }

        return Interned::bool(\true);

    }

    /**
     * This function returns `true` if a `bool` value passed into it is `true`
     * and throws error if it's `false`. Optional `string` description can be
     * provided, which will be visible in the eventual error message.
     */
    #[PrimiFunc(callConv: PrimiFunc::CONV_CALLARGS)]
    public static function range(CallArgs $args): IteratorFactoryValue {

        $args = $args->extract(['start', 'end', 'step'], ['end', 'step']);

        // No explicit 'end' argument? That means 'start' actually means 'end'.
        if (!isset($args['end'])) {
            $end = $args['start']->getCoreValue();
            $start = '0';
        } else {
            $start = $args['start']->getCoreValue();
            $end = $args['end']->getCoreValue();
        }

        $step = isset($args['step']) ? $args['step']->getCoreValue() : '1';

        if (
            !Func::is_round_int($start)
            || !Func::is_round_int($end)
            || !Func::is_round_int($step)
        ) {
            throw new RuntimeError(
                "All numbers passed to range() must be integers");
        }

        if ($step <= 0) {
            throw new RuntimeError(
                "Range must have a non-negative non-zero step");
        }

        $direction = $end >= $start ? 1 : -1;
        $step *= $direction;

        $gen = function(int $start, int $end, int $step) {

            if ($start === $end) {
                return;
            }

            $c = $start;
            while (\true) {

                if ($start < $end && $c >= $end) {
                    break;
                }

                if ($start > $end && $c <= $end) {
                    break;
                }

                yield Interned::number((string) $c);
                $c += $step;

            }

            return;

        };

        return new IteratorFactoryValue(
            // Short closure syntax gives latest PHPStan a headache and
            // reports some nonsense about the function "should not return
            // anything".
            function() use ($gen, $start, $end, $step): \Generator {
                return $gen((int) $start, (int) $end, (int) $step);
            },
            'range'
        );

    }

    /**
     * Return list of names of attributes present in an object.
     */
    #[PrimiFunc]
    public static function dir(AbstractValue $value): ListValue {
        return new ListValue(
            array_map(
                [Interned::class, 'string'],
                $value->dirItems() ?? []
            )
        );
    }

    /**
     * Returns iterator yielding tuples of index and items from an iterator.
     *
     * ```js
     * a_list = ['a', 'b', 123, false]
     * list(enumerate(a_list)) == [(0, 'a'), (1, 'b'), (2, 123), (3, false)]
     *
     * b_list = ['a', 'b', 123, false]
     * list(enumerate(b_list, -5)) == [(-5, 'a'), (-4, 'b'), (-3, 123), (-2, false)]
     * ```
     */
    #[PrimiFunc(toStack: \true, callConv: PrimiFunc::CONV_CALLARGS)]
    public static function enumerate(CallArgs $args): IteratorFactoryValue {

        [$iterable, $start] = $args->extractPositional(2, 1);
        $start ??= Interned::number('0');

        if (!$start instanceof NumberValue) {
            throw new RuntimeError("Start must be a number");
        }

        $counter = $start->getStringValue();
        $it = function($iterable, $counter): \Generator {

            $iter = $iterable->getIterator();
            if ($iter === \null) {
                throw new RuntimeError("Passed iterable is not iterable");
            }

            foreach ($iter as $item) {
                yield new TupleValue([
                    Interned::number((string) $counter),
                    $item
                ]);
                $counter++;
            }

        };

        return new IteratorFactoryValue(
            // Short closure syntax gives latest PHPStan a headache and
            // reports some nonsense about the function "should not return
            // anything".
            function() use ($it, $iterable, $counter): \Generator {
                return $it($iterable, $counter);
            },
            'enumerate'
        );

    }

    /**
     * Returns `true` if object has an attribute with specified name.
     */
    #[PrimiFunc]
    public static function hasattr(
        AbstractValue $obj,
        StringValue $name
    ): BoolValue {
        return Interned::bool(isset($obj->attrs[$name->getStringValue()]));
    }

    /**
     * Returns value of object's attribute with specified name. If the object
     * has no attribute of that name, error is thrown.
     *
     * If the optional `default` argument is specified its value is returned
     * instead of throwing an error.
     */
    #[PrimiFunc(callConv: PrimiFunc::CONV_CALLARGS)]
    public static function getattr(
        CallArgs $args
    ): AbstractValue {

        [$obj, $name, $default] = $args->extractPositional(3, 1);

        $attrName = $name->getStringValue();
        if ($attrValue = $obj->attrGet($attrName)) {
            return $attrValue;
        }

        if ($default !== \null) {
            return $default;
        }

        $typeName = $obj->getTypeName();
        throw new LookupError(
            "Object of type '$typeName' has no attribute '$attrName'");

    }

};