trampoline-ts
Version:
A type-safe way to emulate tail-call optimization with trampolines
95 lines • 4.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const conditional_type_checks_1 = require("conditional-type-checks");
const src_1 = require("../src");
describe('trampoline', () => {
describe('trampoline(fn)', () => {
it('returns a tail call recursive function', () => {
expect(src_1.trampoline(() => true)).toBeInstanceOf(Function);
});
it('preserves argument types in the returned function', () => {
const impl = (_1, _2, _3) => true;
const fn = src_1.trampoline(impl);
// tslint:disable-next-line:ban-types
conditional_type_checks_1.assert(true);
conditional_type_checks_1.assert(true);
});
it('preserves return type in the returned function', () => {
const impl = () => true;
const fn = src_1.trampoline(impl);
// tslint:disable-next-line:ban-types
conditional_type_checks_1.assert(true);
conditional_type_checks_1.assert(true);
});
it('removes "ThunkOrValue" from the returned functions return type', () => {
const impl = () => true;
const fn = src_1.trampoline(impl);
// tslint:disable-next-line:ban-types
conditional_type_checks_1.assert(true);
conditional_type_checks_1.assert(true);
});
it('preserves argument types in "cont"', () => {
const impl = (_1, _2, _3) => true;
const { cont } = src_1.trampoline(impl);
// tslint:disable-next-line:ban-types
conditional_type_checks_1.assert(true);
conditional_type_checks_1.assert(true);
});
it('preserves return type in "cont" returned thunk', () => {
const impl = () => true;
const { cont } = src_1.trampoline(impl);
const thunk = cont();
// tslint:disable-next-line:ban-types
conditional_type_checks_1.assert(true);
conditional_type_checks_1.assert(true);
});
it('returns a function with a thunk returning "cont" method', () => {
const fn = jest.fn((input) => `${input}!`);
const { cont } = src_1.trampoline(fn);
const thunk = cont('input');
expect(src_1.isThunk(thunk)).toBe(true);
expect(fn).not.toHaveBeenCalled();
expect(thunk()).toBe('input!');
expect(fn).toHaveBeenNthCalledWith(1, 'input');
});
it(`loops until the passed function doesn't return a thunk`, () => {
const timesToLoop = 5;
const fn = src_1.trampoline((times = 0) => {
return times < timesToLoop ? fn.cont(times + 1) : times;
});
const contSpy = jest.spyOn(fn, 'cont');
fn();
expect(contSpy).toHaveBeenCalledTimes(5);
});
it(`doesn't throw a stack overflow error`, () => {
const brokenFactorial = (n, acc = 1) => {
return n ? brokenFactorial(n - 1, acc * n) : acc;
};
const factorial = src_1.trampoline((n, acc = 1) => {
return n
? factorial.cont(n - 1, acc * n)
: acc;
});
expect(() => brokenFactorial(32768)).toThrowError('Maximum call stack size exceeded');
expect(factorial(32768)).toEqual(Infinity);
});
it('supports returning functions', () => {
const fn = src_1.trampoline(() => {
return () => 'hello';
});
expect(fn()).toBeInstanceOf(Function);
expect(fn()()).toBe('hello');
});
it('supports async functions', async () => {
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const factorial = src_1.trampolineAsync(async (n, acc = 1) => {
await sleep(10);
return n
? factorial.cont(n - 1, acc * n)
: acc;
});
expect(await factorial(2)).toBe(2);
});
});
});
//# sourceMappingURL=trampoline.spec.js.map