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