* * Full license available in separate LICENSE file */ use Kadet\Functional as f; use Kadet\Functional\Compilable; use Kadet\Functional\Decorator; use Kadet\Functional\Predicate\ClosurePredicate; use Kadet\Functional\Predicates as p; use Kadet\Functional\Utils\FunctionPipeline; use Kadet\Functional\Utils\PartiallyAppliedFunction; use Kadet\Functional\Utils\UnpackedArgs; 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 testCompileWithFewerArgumentsThanPlaceholdersMatchesDecorator() { $partial = f\partial(str_repeat(...), _, _); $this->assertSame($this->outcome(fn () => $partial('a')), $this->outcome(fn () => f\compile($partial)('a'))); } private function outcome(callable $call) { try { return ['result', $call()]; } catch (\Throwable $error) { return ['error', get_class($error), $error->getMessage()]; } } 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)); } public function testPipelineAndPredicatesAreCompilable() { $this->assertInstanceOf(Compilable::class, f\pipe('strtoupper')); $this->assertInstanceOf(Compilable::class, f\predicate(fn ($x) => $x)); $this->assertInstanceOf(Compilable::class, f\not(fn ($x) => $x)); $this->assertInstanceOf(Compilable::class, f\all(fn ($x) => $x)); $this->assertInstanceOf(Compilable::class, f\any(fn ($x) => $x)); $this->assertInstanceOf(Compilable::class, f\always()); } public function testCompiledPipelineDoesNotInvokePipeline() { $spy = new class(trim(...), strtoupper(...)) extends FunctionPipeline { public int $calls = 0; public function __invoke(...$args) { $this->calls++; return parent::__invoke(...$args); } }; $compiled = $spy->compile(); $this->assertSame('A', $compiled(' a ')); $this->assertSame(0, $spy->calls); } public function testCompiledPipelineMatchesPipeline() { $pipelines = [ f\pipe(), f\pipe(fn (...$args) => $args), f\pipe(trim(...), strtoupper(...)), f\pipe(fn ($a, $b) => $a + $b, fn ($x) => $x * 2, fn ($x) => $x - 1), f\pipe(fn ($a, $b) => new UnpackedArgs([$b, $a]), fn ($a, $b) => "$a$b"), ]; foreach ($pipelines as $pipeline) { $this->assertEquals($pipeline(1, 2), f\compile($pipeline)(1, 2)); } } public function testCompiledPipelineKeepsCurrying() { $pipeline = f\pipe(strtoupper(...), f\curry(fn ($a, $b) => "$a$b")); $this->assertSame($pipeline('a')('b'), f\compile($pipeline)('a')('b')); } public function testCompileUnwrapsClosureOfCompilable() { $spy = new class(strtoupper(...)) extends FunctionPipeline { public int $calls = 0; public function __invoke(...$args) { $this->calls++; return parent::__invoke(...$args); } }; $this->assertSame('A', f\compile(\Closure::fromCallable($spy))('a')); $this->assertSame('A', f\compile($spy(...))('a')); $this->assertSame(0, $spy->calls); } public function testCompiledPredicatesMatchPredicates() { $values = [0, 1, 2, 3, '1', 'x']; $cases = [ [f\predicate(fn ($x) => $x), $values], [f\predicate(is_numeric(...)), $values], [p\gt(1), $values], [p\gt(1)->not(), $values], [f\not(fn ($x) => $x > 1), $values], [f\all(p\gt(0), p\lt(3)), $values], [f\any(p\lt(1), p\gt(2)), $values], [f\always(), $values], [f\never(), $values], [p\property('x', p\gt(1)), [(object) ['x' => 1], (object) ['x' => 2]]], ]; foreach ($cases as [$predicate, $inputs]) { $compiled = f\compile($predicate); foreach ($inputs as $value) { $this->assertSame($predicate($value), $compiled($value)); } } } public function testCompiledPredicateDoesNotInvokePredicate() { $inner = new class(fn ($x) => $x > 1) extends ClosurePredicate { public int $calls = 0; public function __invoke(...$args): bool { $this->calls++; return parent::__invoke(...$args); } }; $compiled = f\compile(f\all($inner->not(), p\lt(5))); $this->assertTrue($compiled(1)); $this->assertFalse($compiled(2)); $this->assertSame(0, $inner->calls); } public function testUnaryCompiledCallablesMatchWithExtraArguments() { $callables = [ f\pipe(fn ($x) => $x, strtoupper(...)), f\predicate(fn ($x) => $x === 'a'), f\not(fn ($x) => $x === 'a'), f\all(fn ($x) => $x === 'a', p\same('a')), f\any(fn ($x) => $x === 'b', p\same('a')), ]; foreach ($callables as $callable) { $this->assertSame($callable('a', 'extra'), f\compile($callable)('a', 'extra')); } } public function testCompiledEmptyCombinatorsTakeNoArguments() { $this->assertSame(f\all()(), f\compile(f\all())()); $this->assertSame(f\any()(), f\compile(f\any())()); } }