git.fiddlerwoaroof.com
Raw Blame History
import * as uut from "./genfuns";
import { fail } from "assert";

describe("matches_specializer", () => {
  test("works in expected cases", () => {
    function AThing() {}
    const an_instance = new AThing();

    expect(uut.matches_specializer(an_instance, AThing)).toBeTruthy();
    expect(uut.matches_specializer(an_instance, String)).toBeFalsy();
    expect(uut.matches_specializer(an_instance, Object)).toBeTruthy();

    expect(uut.matches_specializer([], Array)).toBeTruthy();
    expect(uut.matches_specializer([], Object)).toBeTruthy();
    expect(uut.matches_specializer([], Number)).toBeFalsy();

    function Foo() {}
    Foo.prototype = Object.create(null);
    const inst = new Foo();
    expect(uut.matches_specializer(inst, Foo)).toBeTruthy();
    expect(uut.matches_specializer(inst, Object)).toBeFalsy();

    expect(uut.matches_specializer({ a: 1 }, uut.Shape("a"))).toBeTruthy();
    expect(
      uut.matches_specializer({ a: 1, b: 2 }, uut.Shape("a"))
    ).toBeTruthy();
    expect(uut.matches_specializer({ b: 2 }, uut.Shape("a"))).toBeFalsy();

    expect(
      uut.matches_specializer({ a: 1, b: 2, c: 3 }, uut.Shape("a", "b", "c"))
    ).toBeTruthy();
    expect(
      uut.matches_specializer(
        { a: 1, b: 2, c: 3, d: 4 },
        uut.Shape("a", "b", "c")
      )
    ).toBeTruthy();
    expect(
      uut.matches_specializer({ a: 1, c: 3 }, uut.Shape("a", "b", "c"))
    ).toBeFalsy();
    expect(
      uut.matches_specializer({ c: 3 }, uut.Shape("a", "b", "c"))
    ).toBeFalsy();
    expect(
      uut.matches_specializer({ d: 3 }, uut.Shape("a", "b", "c"))
    ).toBeFalsy();
  });

  describe("primitives", () => {
    test("null behavior", () => {
      expect(uut.matches_specializer(null, null)).toBeTruthy();
      expect(uut.matches_specializer(null, Number)).toBeFalsy();
      expect(uut.matches_specializer(null, String)).toBeFalsy();
      expect(uut.matches_specializer(null, Object)).toBeTruthy();
    });

    test("undefined (the value) behavior", () => {
      expect(uut.matches_specializer(undefined, undefined)).toBeTruthy();
      expect(uut.matches_specializer(undefined, Number)).toBeFalsy();
      expect(uut.matches_specializer(undefined, String)).toBeFalsy();
      expect(uut.matches_specializer(undefined, Object)).toBeTruthy();
    });

    test.each([true, false])("booleans -> %s", bool => {
      expect(bool).not.toBe(Object(bool));
      expect(uut.matches_specializer(new Boolean(bool), Boolean)).toBeTruthy();
      expect(uut.matches_specializer(bool, Boolean)).toBeTruthy();
      expect(uut.matches_specializer(bool, Object)).toBeTruthy();
    });

    test("works for numbers", () => {
      expect(1).not.toBe(Object(1));
      expect(uut.matches_specializer(new Number(1), Number)).toBeTruthy();
      expect(uut.matches_specializer(new Number(1), Object)).toBeTruthy();
      expect(uut.matches_specializer(new Number(1), String)).toBeFalsy();

      expect(uut.matches_specializer(1, Number)).toBeTruthy();
      expect(uut.matches_specializer(1, Object)).toBeTruthy();
      expect(uut.matches_specializer(1, String)).toBeFalsy();
    });

    test("handles strings", () => {
      expect("asdf").not.toBe(Object("asdf"));

      expect(
        uut.matches_specializer(new String("foobar"), String)
      ).toBeTruthy();
      expect(
        uut.matches_specializer(new String("foobar"), Object)
      ).toBeTruthy();

      expect(uut.matches_specializer("1", String)).toBeTruthy();
      expect(uut.matches_specializer("1", Object)).toBeTruthy();
      expect(uut.matches_specializer("1", Number)).toBeFalsy();
    });

    test("handles symbols", () => {
      const symbolPrim = Symbol("primitive");
      const boxedSymbol = Object(symbolPrim);
      expect(symbolPrim).not.toBe(boxedSymbol);

      expect(uut.matches_specializer(boxedSymbol, Symbol)).toBeTruthy();
      expect(uut.matches_specializer(boxedSymbol, Object)).toBeTruthy();

      expect(uut.matches_specializer(symbolPrim, Symbol)).toBeTruthy();
      expect(uut.matches_specializer(symbolPrim, Object)).toBeTruthy();
    });

    test("handles BigInt", () => {
      expect(BigInt(4)).not.toBe(Object(BigInt(4)));

      expect(uut.matches_specializer(Object(BigInt(4)), BigInt)).toBeTruthy();
      expect(uut.matches_specializer(Object(BigInt(4)), Object)).toBeTruthy();

      expect(uut.matches_specializer(BigInt(4), BigInt)).toBeTruthy();
      expect(uut.matches_specializer(BigInt(4), Object)).toBeTruthy();
    });
  });

  test("works with custom specializers", () => {
    const AEql = makeCustomSpecializer();
    expect(uut.matches_specializer("foo", new AEql("foo"))).toBeTruthy();
  });
});

describe("defgeneric", () => {
  test("methods get called appropriately", () => {
    expect(
      uut
        .defgeneric("testing1", "a", "b")
        .primary([Object, Object], (_, __) => 1)
        .fn(1, 2)
    ).toEqual(1);

    expect(() => {
      uut
        .defgeneric("foobar", "a")
        .primary([String], function (a) {})
        .fn({});
    }).toThrow(uut.NoApplicableMethodError);

    expect(
      uut
        .defgeneric("testing1", "a", "b")
        .primary([Number, Number], (_, __) => 1)
        .fn(1, 2)
    ).toEqual(1);

    expect(
      uut
        .defgeneric("testing1", "a", "b")
        .primary([Number, Number], (_, __) => 2)
        .primary([String, String], (_, __) => 1)
        .fn("1", "2")
    ).toEqual(1);

    let firstCounts = 0;
    expect(
      uut
        .defgeneric("testing1", "a", "b")
        .primary([Number, Number], (_, __) => (firstCounts += 1))
        .primary([String, String], (_, __) => (firstCounts += 1))
        .fn("1", "2")
    ).toEqual(1);
    expect(firstCounts).toEqual(1);

    let secondCounts = 0;
    expect(
      uut
        .defgeneric("testing1", "a", "b")
        .primary([Object, Object], (_, __) => (secondCounts += 1))
        .primary([String, String], (_, __) => (secondCounts += 1))
        .fn("1", "2")
    ).toEqual(1);
    expect(secondCounts).toEqual(1);

    let thirdCounts = 0;
    expect(
      uut
        .defgeneric("testing1", "a", "b")
        .before([Object, Object], (_, __) => (thirdCounts += 1))
        .primary([String, String], (_, __) => "hi")
        .after([Object, String], (_, __) => (thirdCounts += 1))
        .fn("1", "2")
    ).toEqual("hi");
    expect(thirdCounts).toEqual(2);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([Object], function (a) {
          return 1;
        })
        .primary([String], function (a) {
          return 2;
        })
        .fn("foobar")
    ).toEqual(2);
  });

  test("works with custom specializers", () => {
    const AEql = makeCustomSpecializer();

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([new AEql("foo")], function (a) {
          return 3;
        })
        .fn("foo")
    ).toEqual(3);

    expect(new AEql("foo").super_of(String)).toBeFalsy();
    expect(new AEql("foo").super_of(Object)).toBeFalsy();

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([Object], function (a) {
          return 1;
        })
        .primary([String], function (a) {
          return 2;
        })
        .primary([new AEql("foo")], function (a) {
          return 3;
        })
        .fn("foobar")
    ).toEqual(2);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([Object], function (a) {
          return 1;
        })
        .primary([String], function (a) {
          return 2;
        })
        .primary([new AEql("foo")], function (a) {
          return 3;
        })
        .fn("foo")
    ).toEqual(3);
  });

  test("next-method-p works", () => {
    expect.assertions(3);

    uut
      .defgeneric("foobar", "a")
      .primary([Object], function (a) {
        expect(this.next_method_p).toBe(false);
      })
      .fn({});

    uut
      .defgeneric("foobar", "a")
      .primary([Object], function (a) {
        expect(this.next_method_p).toBe(false);
      })
      .primary([String], function (a) {
        expect(this.next_method_p).toBe(true);
      })
      .fn("foobar");

    uut
      .defgeneric("foobar", "a")
      .primary([Object], function (a) {
        expect(this.next_method_p).toBe(false);
      })
      .primary([String], function (a) {
        expect(this.next_method_p).toBe(true);
      })
      .fn(1);
  });

  test("call-next-method works", () => {
    expect(() => {
      uut
        .defgeneric("foobar", "a")
        .primary([Object], function (a) {
          this.call_next_method();
        })
        .primary([String], function (a) {
          return 1;
        })
        .fn({});
    }).toThrow(uut.NoNextMethodError);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([Object], function (a) {
          return 1;
        })
        .primary([String], function (a) {
          return this.call_next_method();
        })
        .fn("foobar")
    ).toEqual(1);

    expect(
      uut
        .defgeneric("foobar", "a", "b")
        .primary([String, String], function (a, b) {
          return `1${this.call_next_method()}`;
        })
        .primary([Object, String], function (a, b) {
          return `3${this.call_next_method()}`;
        })
        .primary([String, Object], function (a, b) {
          return `2${this.call_next_method()}`;
        })
        .primary([Object, Object], function (a, b) {
          return `4`;
        })
        .fn("a", "b")
    ).toEqual("1234");

    try {
      uut
        .defgeneric("foobar", "a")
        .primary([Object], function (a) {
          this.call_next_method();
        })
        .fn({});
      fail();
    } catch (err) {
      expect(err).toBeInstanceOf(uut.NoNextMethodError);
    }
  });

  test("bugfix: behavior of null in method", () => {
    const expectedResult = Symbol(4);
    expect(() => {
      uut
        .defgeneric("foobar", "a", "b")
        .primary([Object, Object], () => expectedResult)
        .fn(null, null);
    }).not.toThrow();
    expect(
      uut
        .defgeneric("foobar", "a", "b")
        .primary([Object, Object], () => expectedResult)
        .fn(null, null)
    ).toBe(expectedResult);
  });
});

describe("custom specializers", () => {
  test("Shape works", () => {
    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape("a", "b")], ({ a, b }) => a + b)
        .primary([Object], _ => null)
        .fn({ a: 1, b: 2 })
    ).toEqual(3);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape("a", "b")], ({ a, b }) => a + b)
        .primary([Object], _ => null)
        .fn({ a: 1, b: 2, c: 3 })
    ).toEqual(3);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape("a", "b")], ({ a, b }) => a + b)
        .primary([Object], _ => null)
        .fn({ a: 1 })
    ).toEqual(null);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape(["a", 1], "b")], ({ a, b }) => a + b)
        .primary([Object], _ => null)
        .fn({ a: 1, b: 3 })
    ).toEqual(4);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape(["a", null], "b")], ({ a, b }) => b)
        .primary([Object], _ => null)
        .fn({ a: null, b: 3 })
    ).toEqual(3);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape(["a", undefined], "b")], ({ a, b }) => b)
        .primary([Object], _ => null)
        .fn({ b: 5 })
    ).toEqual(null); //undefined is not a permissible default: treated as if the key is missing

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape(["a", 1], "b")], ({ a, b }) => a + b)
        .primary([Object], _ => null)
        .fn({ a: 2, b: 3 })
    ).toEqual(null);
  });

  test("Shape, prototype precedence", () => {
    expect(
      uut
        .defgeneric("foobar4", "a")
        .primary([uut.Shape("a")], ({ a }) => a)
        .primary([uut.Shape("a", "b")], ({ a, b }) => {
          return a + b;
        })
        .primary([Object], _ => null)
        .fn({ a: 1, b: 3 })
    ).toEqual(4);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape("a", "b")], ({ a, b }) => a + b)
        .primary([uut.Shape("b")], ({ b }) => b)
        .primary([Object], _ => null)
        .fn({ a: 1, b: 2 })
    ).toEqual(3);

    const Foo = function () {};
    Foo.prototype = { a: true, b: null };
    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([uut.Shape("a")], function ({ a }) {
          return `a${this.call_next_method()}`;
        })
        .primary([uut.Shape("a", "b", "c")], function ({ a, b, c }) {
          return `c${this.call_next_method()}`;
        })
        .primary([uut.Shape("a", "b")], function ({ a, b }) {
          return `b${this.call_next_method()}`;
        })
        .primary([Object], _ => "d")
        .fn(Object.assign(new Foo(), { c: 3 }))
    ).toEqual("cbad");
  });
});

describe("Eql", () => {
  test("basic stuff works", () => {
    expect(uut.matches_specializer("foo", uut.Eql("foo"))).toBeTruthy();

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([new uut.Eql("foo")], function (a) {
          return 3;
        })
        .fn("foo")
    ).toEqual(3);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([new uut.Eql(5)], function (a) {
          return 3;
        })
        .fn(5)
    ).toEqual(3);
  });
  test("inheritance works", () => {
    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([Symbol], function () {
          return 2;
        })
        .primary([uut.Eql(Symbol.iterator)], function () {
          return 3;
        })
        .fn(Symbol.iterator)
    ).toEqual(3);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([Boolean], function (a) {
          return 2;
        })
        .primary([new uut.Eql(false)], function (a) {
          return 3;
        })
        .fn(false)
    ).toEqual(3);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([Boolean], function (a) {
          return 2;
        })
        .primary([new uut.Eql(true)], function (a) {
          return 3;
        })
        .fn(true)
    ).toEqual(3);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([String], function (a) {
          return 2;
        })
        .primary([new uut.Eql("5")], function (a) {
          return 3;
        })
        .fn("5")
    ).toEqual(3);

    expect(
      uut
        .defgeneric("foobar", "a")
        .primary([Number], function (a) {
          return 2;
        })
        .primary([new uut.Eql(5)], function (a) {
          return 3;
        })
        .fn(5)
    ).toEqual(3);
  });
});

describe("Shape", () => {
  test("super_of", () => {
    expect(uut.Shape().super_of(uut.Shape("a", "b", "c"))).toBeTruthy();
    expect(uut.Shape("a").super_of(uut.Shape("a", "b"))).toBeTruthy();
    expect(uut.Shape("a", "b").super_of(uut.Shape("a", "b", "c"))).toBeTruthy();
    expect(uut.Shape("a", "b").super_of(uut.Shape("a", "b", 3))).toBeTruthy();
    expect(uut.Shape("a", "b").super_of(uut.Shape("a", "b"))).toBeFalsy();
    expect(uut.Shape("a", "b").super_of(uut.Shape("a"))).toBeFalsy();
  });
});

function makeCustomSpecializer() {
  function AEql(val) {
    this.val = val;
  }
  AEql.prototype = Object.assign(new uut.Specializer(), {
    toString() {
      return `AEql(${this.val})`;
    },
    matches(other) {
      return this.val === other;
    },
    super_of(other) {
      return other === String ? false : other !== Object;
    },
  });
  return AEql;
}