packages/deriver/src/Deriver/Evaluator/CallEvaluation.php

1<?php
2
3declare(strict_types=1);
4
5namespace Deriver\Deriver\Evaluator;
6
7use Deriver\Deriver\Evaluator\Builtin\Library;
8use Deriver\Deriver\Evaluator\Call\ArgumentBindings;
9use Deriver\Deriver\Evaluator\Call\ArgumentDemand;
10use Deriver\Deriver\Evaluator\Call\BoundArgument;
11use Deriver\Deriver\Evaluator\Call\CallDescription;
12use Deriver\Deriver\Evaluator\Call\Plan\PlanCompiler;
13use Deriver\Deriver\Evaluator\Call\Plan\PlanValidation;
14use Deriver\Deriver\Evaluator\Declaration\Declarations;
15use Deriver\Deriver\Evaluator\Index\BodyIndex;
16use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
17use Deriver\Deriver\Lowerer\Graph\Instruction;
18use Deriver\Deriver\Sources\SourceRef;
19use Override;
20use Throwable;
21
22/**
23 * Selects a replacement before demanding any source body or irrelevant input.
24 * @implements Micro\MicroEvaluator<BodyIndex|Term|null>
25 * @visibility namespace
26 */
27final class CallEvaluation implements Micro\MicroEvaluator
28{
29    /**
30     * Captures the dependencies used by this component.
31     */
32    public function __construct(public readonly Evaluator $engine)
33    {
34    }
35
36    /**
37     * Evaluates a resolved call through the common micro evaluator contract.
38     * @throws \Deriver\Exception\EvaluationContractException If an evaluator violates its contract
39     */
40    #[Override]
41    public function evaluate(Micro\Request $request): BodyIndex|Term|null
42    {
43        $caller = $request->frame;
44        $call = $request->instruction();
45        $target = $request->function ?? $call->name;
46        $depth = $request->depth;
47        $context = $this->engine->context;
48        $key = (new CallableIdentity())->key($target);
49        $evaluator = $context->evaluators->evaluators[$key] ?? ($context->configuration->standardEvaluators ? (new Library())->evaluator($target) : null);
50        if ($evaluator === null) {
51            return null;
52        }
53        $descriptor = $evaluator->descriptor();
54        $description = new CallDescription($target, $descriptor->signature, $context->configuration->target, source: $call->source, declarations: new Declarations($context->index->program));
55        if ($evaluator instanceof ArgumentDemand) {
56            $description = $this->demand($evaluator, $descriptor, $description, $caller, $call, $depth);
57        }
58
59        try {
60            $decision = $evaluator->evaluate($description);
61        } catch (Throwable $error) {
62            throw new \Deriver\Exception\EvaluationContractException($descriptor->id . ': ' . $error->getMessage(), previous: $error);
63        }
64        if ($decision->kind === Call\DecisionKind::Declined) {
65            return null;
66        }
67        $context->evaluatorApplications++;
68        if ($decision->kind !== Call\DecisionKind::Handled || $decision->plan === null) {
69            return $context->reference($caller, 'call:' . $target, $call->source, reason: 'UNSUPPORTED_EVALUATOR_CASE');
70        }
71        $violations = (new PlanValidation())->validate($decision->plan, $descriptor->signature, $context->evaluators->state->slots);
72        if ($violations !== []) {
73            throw new \Deriver\Exception\EvaluationContractException($descriptor->id . ': ' . implode(', ', $violations));
74        }
75        $source = new SourceRef($call->source->snapshotId, 'evaluator:' . $descriptor->id . '@' . $descriptor->version, 0, 0);
76        $declaration = $descriptor->useSourceSignature ? $context->index->program->callable($target) : null;
77        $body = (new PlanCompiler($source))->compile($descriptor, $decision->plan, $declaration);
78        $context->evaluationEvidence->offsetSet($body, new Evidence\Node(Evidence\EvidenceKind::EvaluatorApplication, $this->engine->context->provenance->inputs(array_map(static fn (BoundArgument $argument): Term => $argument->value, $description->arguments->arguments)), $call->source, ['id' => $descriptor->id, 'version' => $descriptor->version, 'operation' => $call->operation, 'name' => $target]));
79        return new BodyIndex($body);
80    }
81
82    /**
83     * Requests only parameters used to select a trusted evaluator plan.
84     * @throws \Deriver\Exception\EvaluationContractException If the evaluator violates its demand contract
85     */
86    public function demand(ArgumentDemand $evaluator, Call\CallDescriptor $descriptor, CallDescription $description, Scope\Frame $caller, Instruction $call, int $depth): CallDescription
87    {
88        $context = $this->engine->context;
89        $arguments = $description->arguments->arguments;
90        try {
91            $demanded = $evaluator->demand($description);
92        } catch (Throwable $error) {
93            throw new \Deriver\Exception\EvaluationContractException($descriptor->id . ': ' . $error->getMessage(), previous: $error);
94        }
95        $actuals = $demanded === [] ? [] : (new Invocation\Arguments($this->engine))->actuals($caller, $call, $depth);
96        foreach ($demanded as $name) {
97            $position = array_search($name, array_column($descriptor->signature->parameters, 'name'), true);
98            if ($position === false) {
99                throw new \Deriver\Exception\EvaluationContractException($descriptor->id . ': demand names an absent parameter ' . $name);
100            }
101            $actual = $actuals[$name] ?? $actuals[$position] ?? $actuals['*'] ?? null;
102            $value = $actual instanceof Scope\Binding ? $actual->value($this->engine, 'mixed', $depth) : ($actual ?? $descriptor->signature->parameters[$position]->default ?? Term::parameter($name));
103            $arguments[$name] = new BoundArgument($name, $value, supplied: $actual !== null);
104        }
105        return new CallDescription($description->symbol, $descriptor->signature, $context->configuration->target, arguments: new ArgumentBindings($arguments, true), source: $call->source, declarations: $description->declarations);
106    }
107
108    /**
109     * Selects an actual argument by its formal name or position.
110     */
111    public function actual(Instruction $call, string $name, int $position): ?string
112    {
113        foreach ($call->arguments as $index => $argument) {
114            if ($argument->name === $name || $argument->name === null && $index === $position) {
115                return $argument->register;
116            }
117        }
118        return null;
119    }
120}
121