packages/deriver/tests/Unit/Deriver/Evaluator/Execution/Offset/StringsTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Execution\Offset;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\BoundInstruction;
10use Deriver\Deriver\Evaluator\Call\Evaluators;
11use Deriver\Deriver\Evaluator\Call\Extensions;
12use Deriver\Deriver\Evaluator\Call\Registrations;
13use Deriver\Deriver\Evaluator\Call\StateRegistry;
14use Deriver\Deriver\Evaluator\EvaluatorConfig;
15use Deriver\Deriver\Evaluator\Evidence\Frontier;
16use Deriver\Deriver\Evaluator\Execution\Context;
17use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
18use Deriver\Deriver\Evaluator\Execution\Offset\Strings;
19use Deriver\Deriver\Evaluator\Request\Budget;
20use Deriver\Deriver\Evaluator\Request\QueryScope;
21use Deriver\Deriver\Evaluator\Request\ReturnQuery;
22use Deriver\Deriver\Evaluator\Scalar\IntegerConversion;
23use Deriver\Deriver\Evaluator\Scalar\Operations;
24use Deriver\Deriver\Evaluator\TargetProfile;
25use Deriver\Deriver\Evaluator\Term;
26use Deriver\Deriver\Evaluator\TermKind;
27use Deriver\Deriver\Execution\QueryExecution;
28use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
29use Deriver\Deriver\Lowerer\Declaration\CallableSource;
30use Deriver\Deriver\Lowerer\Declaration\Index;
31use Deriver\Deriver\Lowerer\Declaration\Indexer;
32use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
33use Deriver\Deriver\Lowerer\ExpressionLowering;
34use Deriver\Deriver\Lowerer\Graph\BasicBlock;
35use Deriver\Deriver\Lowerer\Graph\CallableGraph;
36use Deriver\Deriver\Lowerer\Graph\Instruction;
37use Deriver\Deriver\Lowerer\Graph\Terminator;
38use Deriver\Deriver\Lowerer\InstructionEmitter;
39use Deriver\Deriver\Lowerer\Lowerer;
40use Deriver\Deriver\Lowerer\LoweringScope;
41use Deriver\Deriver\Lowerer\StatementLowering;
42use Deriver\Deriver\MicroEvaluatorConversion;
43use Deriver\Deriver\Parser\Node\File;
44use Deriver\Deriver\Parser\Parser;
45use Deriver\Deriver\Parser\ParserConfig;
46use Deriver\Deriver\Parser\TargetSyntax;
47use Deriver\Deriver\Providers\ProviderExpansion;
48use Deriver\Deriver\Resolver\AssignmentPatterns;
49use Deriver\Deriver\Resolver\ClassScope;
50use Deriver\Deriver\Resolver\ExpressionValidation;
51use Deriver\Deriver\Resolver\MagicContext;
52use Deriver\Deriver\Resolver\Resolver;
53use Deriver\Deriver\SnapshotIdentity;
54use Deriver\Deriver\Sources\LineMap;
55use Deriver\Deriver\Sources\Path;
56use Deriver\Deriver\Sources\Source;
57use Deriver\Deriver\Sources\SourceRef;
58use Deriver\Deriver\Sources\Sources;
59use Deriver\Exception\ParserException;
60use Deriver\Exception\ResolutionException;
61use PHPUnit\Framework\Attributes\CoversClass;
62use PHPUnit\Framework\Attributes\DataProvider;
63use PHPUnit\Framework\Attributes\Small;
64use PHPUnit\Framework\Attributes\UsesClass;
65use PHPUnit\Framework\TestCase;
66
67/**
68 * @covers \Deriver\Deriver\Evaluator\Execution\Offset\Strings
69 */
70#[UsesClass(Resolver::class)]
71#[UsesClass(ExpressionValidation::class)]
72#[UsesClass(ResolutionException::class)]
73#[UsesClass(ParserConfig::class)]
74#[UsesClass(ParserException::class)]
75#[CoversClass(Strings::class)]
76#[UsesClass(BasicBlock::class)]
77#[UsesClass(CallableGraph::class)]
78#[UsesClass(CallableIdentity::class)]
79#[UsesClass(Instruction::class)]
80#[UsesClass(Terminator::class)]
81#[UsesClass(Context::class)]
82#[UsesClass(Extensions::class)]
83#[UsesClass(Evaluators::class)]
84#[UsesClass(StateRegistry::class)]
85#[UsesClass(DeriverConfig::class)]
86#[UsesClass(Sources::class)]
87#[UsesClass(Path::class)]
88#[UsesClass(Source::class)]
89#[UsesClass(TargetProfile::class)]
90#[UsesClass(Budget::class)]
91#[UsesClass(QueryScope::class)]
92#[UsesClass(ReturnQuery::class)]
93#[UsesClass(SourceRef::class)]
94#[UsesClass(Frontier::class)]
95#[UsesClass(Parser::class)]
96#[UsesClass(File::class)]
97#[UsesClass(Lowerer::class)]
98#[UsesClass(ExpressionLowering::class)]
99#[UsesClass(InstructionEmitter::class)]
100#[UsesClass(LoweringScope::class)]
101#[UsesClass(StatementLowering::class)]
102#[UsesClass(CallableSource::class)]
103#[UsesClass(Indexer::class)]
104#[UsesClass(Index::class)]
105#[UsesClass(Composition::class)]
106#[UsesClass(LineMap::class)]
107#[UsesClass(MagicContext::class)]
108#[UsesClass(AssignmentPatterns::class)]
109#[UsesClass(ClassScope::class)]
110#[UsesClass(TargetSyntax::class)]
111#[UsesClass(IntegerConversion::class)]
112#[UsesClass(Operations::class)]
113#[UsesClass(Term::class)]
114#[Small]
115final class StringsTest extends TestCase
116{
117    public function testIndexDistinguishesExistenceChecksFromReads(): void
118    {
119        $source = '<?php function target(){return 1;}';
120        $budget = new Budget();
121        $configuration = new DeriverConfig();
122        $contributions = new Contributions();
123        $input = new Sources([new Source('fixture.php', $source)]);
124        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
125        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
126        $contributions1 = $providers->contributions;
127        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
128        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
129        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
130        $snapshotId = $identity->id;
131        $input1 = $providers->input;
132        $parser = new Parser(new ParserConfig($target->version));
133        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
134        $program1 = new Index($snapshotId, $trees);
135        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
136        $snapshotId1 = 'test';
137        $input2 = new Sources([new Source('fixture.php', $source)]);
138        $profile = $execution->configuration->target;
139        $parser1 = new Parser(new ParserConfig($profile->version));
140        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
141        $program = new Index($snapshotId1, $trees1);
142        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
143        $body = $context->program->callable('target');
144        self::assertNotNull($body);
145        $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
146        $strings = new Strings($context);
147        self::assertSame('Error', $strings->index(null, $instruction)->literal);
148        self::assertSame('TypeError', $strings->index(Term::array([]), $instruction)->literal);
149        self::assertSame(TermKind::Uninitialized, $strings->index(Term::array([]), $instruction, true)->kind);
150        self::assertSame(1, $strings->index(Term::constant(true), $instruction)->native());
151        self::assertSame(['PHP_WARNING'], array_column(array_values($context->frontiers), 'code'));
152    }
153    public function testStringIndexAcceptsIntegerSpellingsButRejectsDecimalStrings(): void
154    {
155        $source = '<?php function target(){return 1;}';
156        $budget = new Budget();
157        $configuration = new DeriverConfig();
158        $contributions = new Contributions();
159        $input = new Sources([new Source('fixture.php', $source)]);
160        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
161        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
162        $contributions1 = $providers->contributions;
163        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
164        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
165        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
166        $snapshotId = $identity->id;
167        $input1 = $providers->input;
168        $parser = new Parser(new ParserConfig($target->version));
169        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
170        $program1 = new Index($snapshotId, $trees);
171        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
172        $snapshotId1 = 'test';
173        $input2 = new Sources([new Source('fixture.php', $source)]);
174        $profile = $execution->configuration->target;
175        $parser1 = new Parser(new ParserConfig($profile->version));
176        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
177        $program = new Index($snapshotId1, $trees1);
178        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
179        $body = $context->program->callable('target');
180        self::assertNotNull($body);
181        $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
182        $strings = new Strings($context);
183        self::assertSame(1, $strings->stringIndex(Term::constant(' +01 '), $instruction, false)->native());
184        self::assertSame('TypeError', $strings->stringIndex(Term::constant('1.0'), $instruction, false)->literal);
185        self::assertSame(1, $strings->stringIndex(Term::constant('1x'), $instruction, false)->native());
186        self::assertSame(TermKind::Uninitialized, $strings->stringIndex(Term::constant('1x'), $instruction, true)->kind);
187    }
188    public function testReadUsesBytesNegativePositionsAndSilentAbsence(): void
189    {
190        $source = '<?php function target(){return 1;}';
191        $budget = new Budget();
192        $configuration = new DeriverConfig();
193        $contributions = new Contributions();
194        $input = new Sources([new Source('fixture.php', $source)]);
195        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
196        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
197        $contributions1 = $providers->contributions;
198        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
199        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
200        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
201        $snapshotId = $identity->id;
202        $input1 = $providers->input;
203        $parser = new Parser(new ParserConfig($target->version));
204        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
205        $program1 = new Index($snapshotId, $trees);
206        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
207        $snapshotId1 = 'test';
208        $input2 = new Sources([new Source('fixture.php', $source)]);
209        $profile = $execution->configuration->target;
210        $parser1 = new Parser(new ParserConfig($profile->version));
211        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
212        $program = new Index($snapshotId1, $trees1);
213        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
214        $body = $context->program->callable('target');
215        self::assertNotNull($body);
216        $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
217        $strings = new Strings($context);
218        self::assertSame('c', $strings->read(Term::constant('abc'), Term::constant(-1), $instruction)->native());
219        self::assertSame('', $strings->read(Term::constant('abc'), Term::constant(3), $instruction)->native());
220        self::assertSame(TermKind::Uninitialized, $strings->read(Term::constant('abc'), Term::constant(3), $instruction, true)->kind);
221    }
222    public function testWriteReturnsFirstByteAndPreservesTheRemainder(): void
223    {
224        $source = '<?php function target(){return 1;}';
225        $budget = new Budget();
226        $configuration = new DeriverConfig();
227        $contributions = new Contributions();
228        $input = new Sources([new Source('fixture.php', $source)]);
229        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
230        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
231        $contributions1 = $providers->contributions;
232        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
233        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
234        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
235        $snapshotId = $identity->id;
236        $input1 = $providers->input;
237        $parser = new Parser(new ParserConfig($target->version));
238        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
239        $program1 = new Index($snapshotId, $trees);
240        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
241        $snapshotId1 = 'test';
242        $input2 = new Sources([new Source('fixture.php', $source)]);
243        $profile = $execution->configuration->target;
244        $parser1 = new Parser(new ParserConfig($profile->version));
245        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
246        $program = new Index($snapshotId1, $trees1);
247        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
248        $body = $context->program->callable('target');
249        self::assertNotNull($body);
250        $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
251        $write = (new Strings($context))->write(Term::constant('abc'), Term::constant(1), Term::constant('xy'), $instruction);
252        self::assertSame('axc', $write['value']->native());
253        self::assertSame('x', $write['result']->native());
254        self::assertSame(['PHP_WARNING'], array_column(array_values($context->frontiers), 'code'));
255    }
256    public function testWritePadsWithSpacesAndRejectsAnEmptyRightHandString(): void
257    {
258        $source = '<?php function target(){return 1;}';
259        $budget = new Budget();
260        $configuration = new DeriverConfig();
261        $contributions = new Contributions();
262        $input = new Sources([new Source('fixture.php', $source)]);
263        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
264        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
265        $contributions1 = $providers->contributions;
266        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
267        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
268        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
269        $snapshotId = $identity->id;
270        $input1 = $providers->input;
271        $parser = new Parser(new ParserConfig($target->version));
272        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
273        $program1 = new Index($snapshotId, $trees);
274        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
275        $snapshotId1 = 'test';
276        $input2 = new Sources([new Source('fixture.php', $source)]);
277        $profile = $execution->configuration->target;
278        $parser1 = new Parser(new ParserConfig($profile->version));
279        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
280        $program = new Index($snapshotId1, $trees1);
281        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
282        $body = $context->program->callable('target');
283        self::assertNotNull($body);
284        $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
285        $strings = new Strings($context);
286        $write = $strings->write(Term::constant('a'), Term::constant(3), Term::constant('Z'), $instruction);
287        self::assertSame('a  Z', $write['value']->native());
288        $failed = $strings->write(Term::constant('abc'), Term::constant(1), Term::constant(''), $instruction);
289        self::assertSame('abc', $failed['value']->native());
290        self::assertSame('Error', $failed['result']->literal);
291    }
292    public function testConvertRecordsArrayConversionWithoutExecutingObjectCode(): void
293    {
294        $source = '<?php function target(){return 1;}';
295        $budget = new Budget();
296        $configuration = new DeriverConfig();
297        $contributions = new Contributions();
298        $input = new Sources([new Source('fixture.php', $source)]);
299        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
300        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
301        $contributions1 = $providers->contributions;
302        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
303        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
304        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
305        $snapshotId = $identity->id;
306        $input1 = $providers->input;
307        $parser = new Parser(new ParserConfig($target->version));
308        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
309        $program1 = new Index($snapshotId, $trees);
310        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
311        $snapshotId1 = 'test';
312        $input2 = new Sources([new Source('fixture.php', $source)]);
313        $profile = $execution->configuration->target;
314        $parser1 = new Parser(new ParserConfig($profile->version));
315        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
316        $program = new Index($snapshotId1, $trees1);
317        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
318        $body = $context->program->callable('target');
319        self::assertNotNull($body);
320        $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
321        $strings = new Strings($context);
322        self::assertSame('Array', $strings->convert(Term::array([]), $instruction)->native());
323        self::assertSame('OFFSET_OPERATION', $strings->convert(new Term(TermKind::Object, 'id'), $instruction)->literal);
324    }
325    public function testWarningDeduplicatesDiagnosticsAtOneOffsetOperation(): void
326    {
327        $source = '<?php function target(){return 1;}';
328        $budget = new Budget();
329        $configuration = new DeriverConfig();
330        $contributions = new Contributions();
331        $input = new Sources([new Source('fixture.php', $source)]);
332        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
333        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
334        $contributions1 = $providers->contributions;
335        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
336        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
337        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
338        $snapshotId = $identity->id;
339        $input1 = $providers->input;
340        $parser = new Parser(new ParserConfig($target->version));
341        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
342        $program1 = new Index($snapshotId, $trees);
343        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
344        $snapshotId1 = 'test';
345        $input2 = new Sources([new Source('fixture.php', $source)]);
346        $profile = $execution->configuration->target;
347        $parser1 = new Parser(new ParserConfig($profile->version));
348        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
349        $program = new Index($snapshotId1, $trees1);
350        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
351        $body = $context->program->callable('target');
352        self::assertNotNull($body);
353        $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
354        $strings = new Strings($context);
355        $strings->warning($instruction);
356        $strings->warning($instruction);
357        self::assertCount(1, $context->frontiers);
358    }
359    #[DataProvider('providerIndexContracts')]
360    public function testIndexFollowsTheStringOffsetConversionContract(?Term $key, bool $silent, string $kind, mixed $literal, int $warnings): void
361    {
362        $source = '<?php function target(){return 1;}';
363        $budget = new Budget();
364        $configuration = new DeriverConfig();
365        $contributions = new Contributions();
366        $input = new Sources([new Source('fixture.php', $source)]);
367        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
368        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
369        $contributions1 = $providers->contributions;
370        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
371        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
372        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
373        $snapshotId = $identity->id;
374        $input1 = $providers->input;
375        $parser = new Parser(new ParserConfig($target->version));
376        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
377        $program1 = new Index($snapshotId, $trees);
378        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
379        $snapshotId1 = 'test';
380        $input2 = new Sources([new Source('fixture.php', $source)]);
381        $profile = $execution->configuration->target;
382        $parser1 = new Parser(new ParserConfig($profile->version));
383        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
384        $program = new Index($snapshotId1, $trees1);
385        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
386        $instruction = new BoundInstruction('offset', 'read', new SourceRef('test', 'a.php', 0, 1), 'result');
387        $result = (new Strings($context))->index($key, $instruction, $silent);
388        self::assertSame($kind, $result->kind->value);
389        self::assertSame($literal, $result->literal);
390        self::assertCount($warnings, $context->frontiers);
391    }
392    /**
393     * @return iterable<string,array{Term|null,bool,string,mixed,int}>
394     */
395    public static function providerIndexContracts(): iterable
396    {
397        yield 'append' => [null, false, 'throwable', 'Error', 0];
398        yield 'array' => [Term::array([]), false, 'throwable', 'TypeError', 0];
399        yield 'silent array' => [Term::array([]), true, 'uninitialized', null, 0];
400        yield 'object' => [new Term(TermKind::Object, 'o'), false, 'throwable', 'TypeError', 0];
401        yield 'silent object' => [new Term(TermKind::Object, 'o'), true, 'uninitialized', null, 0];
402        yield 'closure' => [new Term(TermKind::Closure, 'c'), false, 'throwable', 'TypeError', 0];
403        yield 'silent enum' => [new Term(TermKind::Enum, 'e'), true, 'uninitialized', null, 0];
404        yield 'integer' => [Term::constant(2), false, 'constant', 2, 0];
405        yield 'negative' => [Term::constant(-2), true, 'constant', -2, 0];
406        yield 'true' => [Term::constant(true), false, 'constant', 1, 1];
407        yield 'silent true' => [Term::constant(true), true, 'constant', 1, 0];
408        yield 'null' => [Term::constant(null), false, 'constant', 0, 1];
409        yield 'silent null' => [Term::constant(null), true, 'constant', 0, 0];
410        yield 'integral float' => [Term::constant(2.0), false, 'constant', 2, 1];
411        yield 'silent integral float' => [Term::constant(2.0), true, 'constant', 2, 0];
412        yield 'silent lossy float' => [Term::constant(2.5), true, 'constant', 2, 1];
413        yield 'integer string' => [Term::constant('02'), false, 'constant', 2, 0];
414        yield 'signed whitespace' => [Term::constant(" \t+2\n"), true, 'constant', 2, 0];
415        yield 'negative string' => [Term::constant('-2'), false, 'constant', -2, 0];
416        yield 'leading integer' => [Term::constant('2tail'), false, 'constant', 2, 1];
417        yield 'silent leading integer' => [Term::constant('2tail'), true, 'uninitialized', null, 0];
418        yield 'decimal' => [Term::constant('2.0'), false, 'throwable', 'TypeError', 0];
419        yield 'leading decimal point' => [Term::constant('.2'), false, 'throwable', 'TypeError', 0];
420        yield 'exponent' => [Term::constant('2e1'), false, 'throwable', 'TypeError', 0];
421        yield 'nonnumeric' => [Term::constant('key'), false, 'throwable', 'TypeError', 0];
422        yield 'silent nonnumeric' => [Term::constant('key'), true, 'uninitialized', null, 0];
423        yield 'maximum' => [Term::constant('9223372036854775807'), false, 'constant', 9223372036854775807, 0];
424        yield 'minimum' => [Term::constant('-9223372036854775808'), false, 'constant', -9223372036854775807 - 1, 0];
425        yield 'positive overflow' => [Term::constant('9223372036854775808'), false, 'throwable', 'TypeError', 0];
426        yield 'negative overflow' => [Term::constant('-9223372036854775809'), true, 'uninitialized', null, 0];
427        yield 'too many digits' => [Term::constant('100000000000000000000'), false, 'throwable', 'TypeError', 0];
428        yield 'zero padded maximum' => [Term::constant('0009223372036854775807'), true, 'constant', 9223372036854775807, 0];
429        yield 'unknown key' => [Term::parameter('key'), false, 'opaque', 'OFFSET_OPERATION', 0];
430    }
431    #[DataProvider('providerByteReads')]
432    public function testReadPreservesByteBoundariesAndAbsentOffsets(string $bytes, int $index, bool $silent, string $kind, mixed $literal, int $warnings): void
433    {
434        $source = '<?php function target(){return 1;}';
435        $budget = new Budget();
436        $configuration = new DeriverConfig();
437        $contributions = new Contributions();
438        $input = new Sources([new Source('fixture.php', $source)]);
439        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
440        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
441        $contributions1 = $providers->contributions;
442        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
443        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
444        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
445        $snapshotId = $identity->id;
446        $input1 = $providers->input;
447        $parser = new Parser(new ParserConfig($target->version));
448        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
449        $program1 = new Index($snapshotId, $trees);
450        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
451        $snapshotId1 = 'test';
452        $input2 = new Sources([new Source('fixture.php', $source)]);
453        $profile = $execution->configuration->target;
454        $parser1 = new Parser(new ParserConfig($profile->version));
455        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
456        $program = new Index($snapshotId1, $trees1);
457        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
458        $instruction = new BoundInstruction('offset', 'read', new SourceRef('test', 'a.php', 0, 1), 'result');
459        $result = (new Strings($context))->read(Term::constant($bytes), Term::constant($index), $instruction, $silent);
460        self::assertSame($kind, $result->kind->value);
461        self::assertSame($literal, $result->literal);
462        self::assertCount($warnings, $context->frontiers);
463    }
464    /**
465     * @return iterable<string,array{string,int,bool,string,mixed,int}>
466     */
467    public static function providerByteReads(): iterable
468    {
469        yield 'first' => ['abc', 0, false, 'constant', 'a', 0];
470        yield 'last' => ['abc', 2, false, 'constant', 'c', 0];
471        yield 'negative last' => ['abc', -1, false, 'constant', 'c', 0];
472        yield 'negative first' => ['abc', -3, false, 'constant', 'a', 0];
473        yield 'past end' => ['abc', 3, false, 'constant', '', 1];
474        yield 'before start' => ['abc', -4, false, 'constant', '', 1];
475        yield 'silent past end' => ['abc', 3, true, 'uninitialized', null, 0];
476        yield 'silent before start' => ['abc', -4, true, 'uninitialized', null, 0];
477        yield 'empty' => ['', 0, false, 'constant', '', 1];
478        yield 'byte sequence' => ['é', 1, false, 'constant', "\xa9", 0];
479        yield 'middle byte' => ['abc', 1, false, 'constant', 'b', 0];
480    }
481    #[DataProvider('providerByteWrites')]
482    public function testWritePreservesTheOriginalOnFailureAndReturnsTheAssignedByte(int $index, mixed $assigned, string $expected, string $kind, mixed $literal, int $warnings): void
483    {
484        $source = '<?php function target(){return 1;}';
485        $budget = new Budget();
486        $configuration = new DeriverConfig();
487        $contributions = new Contributions();
488        $input = new Sources([new Source('fixture.php', $source)]);
489        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
490        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
491        $contributions1 = $providers->contributions;
492        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
493        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
494        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
495        $snapshotId = $identity->id;
496        $input1 = $providers->input;
497        $parser = new Parser(new ParserConfig($target->version));
498        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
499        $program1 = new Index($snapshotId, $trees);
500        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
501        $snapshotId1 = 'test';
502        $input2 = new Sources([new Source('fixture.php', $source)]);
503        $profile = $execution->configuration->target;
504        $parser1 = new Parser(new ParserConfig($profile->version));
505        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
506        $program = new Index($snapshotId1, $trees1);
507        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
508        $instruction = new BoundInstruction('offset', 'write', new SourceRef('test', 'a.php', 0, 1), 'result');
509        $before = Term::constant('abc');
510        $result = (new Strings($context))->write($before, Term::constant($index), Term::fromNative($assigned), $instruction);
511        self::assertSame($expected, $result['value']->native());
512        self::assertSame($kind, $result['result']->kind->value);
513        self::assertSame($literal, $result['result']->literal);
514        self::assertCount($warnings, $context->frontiers);
515        self::assertSame('abc', $before->native());
516    }
517    /**
518     * @return iterable<string,array{int,mixed,string,string,mixed,int}>
519     */
520    public static function providerByteWrites(): iterable
521    {
522        yield 'first' => [0, 'x', 'xbc', 'constant', 'x', 0];
523        yield 'last' => [2, 'x', 'abx', 'constant', 'x', 0];
524        yield 'negative first' => [-3, 'x', 'xbc', 'constant', 'x', 0];
525        yield 'negative last' => [-1, 'x', 'abx', 'constant', 'x', 0];
526        yield 'before start' => [-4, 'x', 'abc', 'constant', null, 1];
527        yield 'append position' => [3, 'x', 'abcx', 'constant', 'x', 0];
528        yield 'pad gap' => [5, 'x', 'abc  x', 'constant', 'x', 0];
529        yield 'empty assignment' => [1, '', 'abc', 'throwable', 'Error', 0];
530        yield 'null assignment' => [1, null, 'abc', 'throwable', 'Error', 0];
531        yield 'long assignment' => [1, 'xyz', 'axc', 'constant', 'x', 1];
532        yield 'integer conversion' => [1, 42, 'a4c', 'constant', '4', 1];
533        yield 'array conversion' => [1, [], 'aAc', 'constant', 'A', 1];
534    }
535    public function testIndexReportsInvalidKeysAsUninitializedWhenSilent(): void
536    {
537        $source = '<?php function target(){return 1;}';
538        $budget = new Budget();
539        $configuration = new DeriverConfig();
540        $contributions = new Contributions();
541        $input = new Sources([new Source('fixture.php', $source)]);
542        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
543        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
544        $contributions1 = $providers->contributions;
545        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
546        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
547        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
548        $snapshotId = $identity->id;
549        $input1 = $providers->input;
550        $parser = new Parser(new ParserConfig($target->version));
551        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
552        $program1 = new Index($snapshotId, $trees);
553        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
554        $snapshotId1 = 'test';
555        $input2 = new Sources([new Source('fixture.php', $source)]);
556        $profile = $execution->configuration->target;
557        $parser1 = new Parser(new ParserConfig($profile->version));
558        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
559        $program = new Index($snapshotId1, $trees1);
560        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
561        $instruction = new BoundInstruction('offset', 'read-silent', new SourceRef('test', 'a.php', 0, 1), 'result');
562        $strings = new Strings($context);
563        $array = new Term(TermKind::Array, attributes: ['open' => false]);
564        $arrayResult = $strings->index($array, $instruction, true);
565        $stringResult = $strings->index(Term::constant('invalid'), $instruction, true);
566        $largeResult = $strings->index(Term::constant('9223372036854775808'), $instruction, true);
567        self::assertSame(TermKind::Uninitialized, $arrayResult->kind);
568        self::assertSame(TermKind::Uninitialized, $stringResult->kind);
569        self::assertSame(TermKind::Uninitialized, $largeResult->kind);
570        self::assertSame([], $context->frontiers);
571    }
572}
573