functional-php/tests/CompileTest.php

165 lines
4.9 KiB
PHP

<?php
/**
* Copyright 2026 Kacper Donat
*
* @author Kacper "Kadet" Donat <kacper@kadet.net>
*
* Full license available in separate LICENSE file
*/
use Kadet\Functional as f;
use Kadet\Functional\Compilable;
use Kadet\Functional\Decorator;
use Kadet\Functional\Utils\PartiallyAppliedFunction;
use const Kadet\Functional\_;
class CompileTest extends \PHPUnit\Framework\TestCase
{
public function testCompileReturnsClosureForPartial()
{
$compiled = f\partial(str_replace(...), 'a', _, 'banana')->compile();
$this->assertInstanceOf(\Closure::class, $compiled);
$this->assertNotInstanceOf(Decorator::class, $compiled);
$this->assertSame('bxnxnx', $compiled('x'));
}
public function testCompiledPartialDoesNotInvokeDecorator()
{
$spy = new class(fn ($a, $b) => $a . $b, 'a') extends PartiallyAppliedFunction {
public int $calls = 0;
public function __invoke(...$args)
{
$this->calls++;
return parent::__invoke(...$args);
}
protected function prepareArguments($args)
{
$this->calls++;
return parent::prepareArguments($args);
}
};
$compiled = $spy->compile();
$this->assertSame('ab', $compiled('b'));
$this->assertSame(0, $spy->calls);
}
public function testCompileMatchesDecoratorForCurried()
{
$curried = f\curry(fn ($a, $b, $c) => "$a$b$c");
$applied = $curried('a');
$compiled = $applied->compile();
$this->assertSame('abc', $compiled('b', 'c'));
$this->assertSame($applied('b', 'c'), $compiled('b', 'c'));
}
public function testCompiledCurriedDoesNotCurryFurther()
{
$applied = f\curry(fn ($a, $b = 'B', $c = 'C') => "$a$b$c", 3, 'a');
$this->assertSame($applied->call('b'), $applied->compile()('b'));
}
public function testCompileNestedPartial()
{
$f3 = fn ($a, $b, $c) => [$a, $b, $c];
$this->assertSame($f3(1, 2, 3), f\compile(f\partial(f\partial($f3, 1), _, 3))(2));
}
public function testCompileAppendsExtraArguments()
{
$partial = f\partial(fn (...$args) => $args, 1, _, 3);
$this->assertSame($partial(2, 4, 5), f\compile($partial)(2, 4, 5));
$this->assertSame([1, 2, 3, 4, 5], f\compile($partial)(2, 4, 5));
}
public function testCompileKeepsUnfilledPlaceholders()
{
$partial = f\partial(fn (...$args) => $args, _, 2, _);
$this->assertSame($partial(1), f\compile($partial)(1));
$this->assertSame($partial(), f\compile($partial)());
}
public function testCompileDropsExtraArgumentsForInternalTarget()
{
$this->assertSame('aa', f\compile(f\partial(str_repeat(...), _, 2))('a', 'extra'));
}
public function testCompileKeepsExtraArgumentsForVariadicInternalTarget()
{
$this->assertSame(['a', 'b', 'c'], f\compile(f\partial(array_merge(...), ['a']))(['b'], ['c']));
}
public function testCompileDropsExtraArgumentsForCurriedInternalTarget()
{
$this->assertSame('aa', f\compile(f\curry(str_repeat(...), null, 'a'))(2, 'extra'));
}
public function testCompileFunctionDelegatesToMethod()
{
$partial = f\partial(str_replace(...), 'a', _, 'banana');
$this->assertSame($partial->compile()('o'), f\compile($partial)('o'));
}
public function testCompilePassesThroughClosures()
{
$closure = fn ($x) => $x;
$this->assertSame($closure, f\compile($closure));
}
public function testCompileConvertsCallableToClosure()
{
$compiled = f\compile('strtoupper');
$this->assertInstanceOf(\Closure::class, $compiled);
$this->assertSame('ABC', $compiled('abc'));
}
public function testDecoratorsAreCompilable()
{
$this->assertInstanceOf(Compilable::class, f\partial('strtoupper'));
$this->assertInstanceOf(Compilable::class, f\curry('strtoupper'));
$this->assertInstanceOf(Compilable::class, f\fix(fn ($f) => $f));
}
public function testCompiledFixedFunctionReferencesItself()
{
$compiled = f\fix(function ($f) use (&$compiled) {
$this->assertSame($f, $compiled);
return fn ($x) => $x;
})->compile();
$this->assertSame(1, $compiled(1));
}
public function testCompiledFixedFunctionRecurses()
{
$compiled = f\compile(f\fix(function ($f) {
return function ($x) use ($f) { return $x >= 2 ? $f($x - 1) * $x : 1; };
}));
$this->assertInstanceOf(\Closure::class, $compiled);
$this->assertSame(6, $compiled(3));
$this->assertSame(24, $compiled(4));
}
public function testCompiledFixedCurriedFunction()
{
$compiled = f\compile(f\fix(f\curry(function ($f, $x) { return $x >= 2 ? $f($x - 1) * $x : 1; })));
$this->assertSame(6, $compiled(3));
$this->assertSame(24, $compiled(4));
}
}