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