git @ Cat's Eye Technologies concoctor / master test / concoctor.test.js
master

Tree @master (Download .tar.gz)

concoctor.test.js @masterraw · history · blame

// Copyright (c) 2026 Chris Pressey, Cat's Eye Technologies.
//
// SPDX-License-Identifier: LicenseRef-MIT-X-concoctor

import { describe, it, expect, assertType } from "vitest";
import { bless, concoct, makeCtx, merge } from "concoctor";

describe("concoctor functions", () => {
  describe("bless", () => {
    it("should allow blessing a native function", () => {
      const add = bless((a, b) => a + b);
      expect(add(2, 3)).toBe(5);
    });

    it("should attach attributes to the returned function", () => {
      const add = bless((a, b) => a + b, {
        name: "add",
        signature: ["number", "number"],
      });
      expect(add.concoctAttributes).toEqual({
        name: "add",
        signature: ["number", "number"],
      });
    });
  });

  describe("makeCtx", () => {
    it("should create a context from blessed functions", () => {
      const add = bless((a, b) => a + b);
      const mul = bless((a, b) => a * b);
      const ctx = makeCtx({ add, mul });

      expect(ctx.get("add")).toBe(add);
      expect(ctx.get("mul")).toBe(mul);
    });

    it("should throw error for non-blessed functions", () => {
      const unblessed = (a, b) => a + b;
      expect(() => makeCtx({ unblessed })).toThrow("Unvetted function");
    });
  });

  describe("merge", () => {
    it("should merge two contexts", () => {
      const add = bless((a, b) => a + b);
      const mul = bless((a, b) => a * b);
      const sub = bless((a, b) => a - b);

      const ctx1 = makeCtx({ add, mul });
      const ctx2 = makeCtx({ sub });
      const merged = merge(ctx1, ctx2);

      expect(merged.get("add")).toBe(add);
      expect(merged.get("mul")).toBe(mul);
      expect(merged.get("sub")).toBe(sub);
    });

    it("should allow same function in both contexts", () => {
      const add = bless((a, b) => a + b);
      const ctx1 = makeCtx({ add });
      const ctx2 = makeCtx({ add });

      expect(() => merge(ctx1, ctx2)).not.toThrow();
    });

    it("should throw error for conflicting definitions", () => {
      const add1 = bless((a, b) => a + b);
      const add2 = bless((a, b) => a + b + 1);

      const ctx1 = makeCtx({ add: add1 });
      const ctx2 = makeCtx({ add: add2 });

      expect(() => merge(ctx1, ctx2)).toThrow("Conflicting definitions");
    });
  });

  describe("concoct", () => {
    it("should be able to create an identity function", () => {
      const square = concoct({}, "(x) => x", {
        parseFunDefn: () => ["", ["x"]],
        compile: () => "x",
      });
      expect(square(5)).toBe(5);
    });

    it("should be able to create a function that calls another function", () => {
      const mul = bless((a, b) => a * b);
      const square = concoct({ mul }, "(a) => mul(a, a)", {
        parseFunDefn: () => ["", ["a"]],
        compile: () => "mul(a, a)",
      });

      expect(square(4)).toBe(16);
      expect(square(5)).toBe(25);
    });

    it("should create a function with multiple parameters", () => {
      const add = bless((a, b) => a + b);
      const mul = bless((a, b) => a * b);
      const compute = concoct({ add, mul }, "(x, y) => mul(add(x, y), 2)", {
        parseFunDefn: () => ["", ["x", "y"]],
        compile: () => "mul(add(x, y), 2)",
      });

      expect(compute(3, 4)).toBe(14); // (3 + 4) * 2
    });

    // FIXME: this is actually implemented in the supplied compiler?
    it.skip("should require that called functions appear in the context", () => {
      expect(() =>
        concoct({}, "(x) => mul(x, 2)", {
          parseFunDefn: () => ["", ["x"]],
          compile: () => "mul(x, 2)",
        }),
      ).toThrowError("Unknown function");
    });

    it("should require that called functions are vetted", () => {
      const mul = (a, b) => a * b;
      expect(() => concoct({ mul }, "(x) => mul(x, 2)")).toThrowError(
        "Unvetted function",
      );
    });

    it("should create a function with no parameters", () => {
      // not, strictly speaking, pure; but we're owning it
      const random = bless(() => Math.random());
      const mul = bless((a, b) => a * b);
      const sqrand = concoct({ mul, random }, "() => mul(random(), random())", {
        parseFunDefn: () => ["", []],
        compile: () => "mul(random(), random())",
      });

      const result = sqrand();
      expect(typeof result).toBe("number");
      expect(result).toBeGreaterThanOrEqual(0);
      expect(result).toBeLessThanOrEqual(1);
    });

    it("should attach attributes to concocted functions", () => {
      const mul = bless((a, b) => a * b);
      const square = concoct({ mul }, "(a) => mul(a, a)", {
        parseFunDefn: () => ["", ["a"]],
        compile: () => "mul(a, a)",
        name: "square",
        signature: ["number"],
      });

      expect(square.concoctAttributes).toEqual({
        name: "square",
        signature: ["number"],
      });
    });
  });
});

describe("Higher-order functions", () => {
  describe("function parameters with signature", () => {
    it("should allow calling a function parameter", () => {
      const inc = bless((x) => x + 1);
      const applyTwice = concoct({}, "(f, x) => f(f(x))", {
        signature: ["function", "number"],
        parseFunDefn: () => ["", ["f", "x"]],
        compile: () => "f(f(x))",
      });

      expect(applyTwice(inc, 5)).toBe(7);
    });

    it("should work with multiple function parameters", () => {
      const add = bless((a, b) => a + b);
      const combine = concoct({}, "(f, g, x) => f(g(x), g(x))", {
        signature: ["function", "function", "number"],
        parseFunDefn: () => ["", ["f", "g", "x"]],
        compile: () => "f(g(x), g(x))",
      });

      const double = bless((x) => x * 2);
      expect(combine(add, double, 3)).toBe(12); // add(double(3), double(3)) = 6 + 6
    });

    it("should work with function parameters and context functions", () => {
      const mul = bless((a, b) => a * b);
      const applyAndSquare = concoct({ mul }, "(f, x) => mul(f(x), f(x))", {
        signature: ["function", "number"],
        parseFunDefn: () => ["", ["f", "x"]],
        compile: () => "mul(f(x), f(x))",
      });

      const inc = bless((x) => x + 1);
      expect(applyAndSquare(inc, 5)).toBe(36); // (5 + 1) * (5 + 1) = 36
    });

    // FIXME: this is actually implemented in the supplied compiler?
    it.skip("should throw error when calling parameter without signature", () => {
      expect(() => {
        concoct({}, "(f, x) => f(x)", {
          parseFunDefn: () => ["", ["f", "x"]],
          compile: () => "f(x)",
        });
      }).toThrow("function signature required");
    });

    // FIXME: this is actually implemented in the supplied compiler?
    it.skip("should throw error when calling non-function parameter", () => {
      expect(() => {
        concoct({}, "(x, y) => x(y)", {
          signature: ["number", "number"],
          parseFunDefn: () => ["", ["x", "y"]],
          compile: () => "x(y)",
        });
      }).toThrow("parameter type is 'number', not 'function'");
    });

    it("should throw error when signature length doesn't match parameters", () => {
      expect(() => {
        concoct({}, "(f, x) => f(x)", {
          signature: ["function"],
          parseFunDefn: () => ["", ["f", "x"]],
          compile: () => "f(x)",
        });
      }).toThrow("Signature length (1) does not match parameter count (2)");
    });

    // FIXME: this is actually implemented in the supplied compiler?
    it.skip("should throw runtime error when passed unvetted function", () => {
      const applyTwice = concoct({}, "(f, x) => f(f(x))", {
        signature: ["function", "number"],
        parseFunDefn: () => ["", ["f", "x"]],
        compile: () => "f(f(x))",
      });

      const regularFunction = (x) => x + 1; // Not blessed
      expect(() => applyTwice(regularFunction, 5)).toThrow("Unvetted function");
    });
  });

  describe("function return values with signature", () => {
    it("should be possible to return a context function", () => {
      const add = bless((a, b) => a + b);
      const sub = bless((a, b) => a - b);
      const select = concoct({ add, sub }, "(o) => o ? add : sub", {
        parseFunDefn: () => ["", ["o"]],
        compile: () => "o ? add : sub",
      });

      expect(select(true)(5, 3)).toBe(8);
      expect(select(false)(5, 3)).toBe(2);
    });

    it("should allow function returning function when properly typed", () => {
      const makeAdder = bless((n) => bless((x) => x + n, { name: "adder" }));
      const applyMaker = concoct({}, "(maker, n, x) => maker(n)(x)", {
        parseFunDefn: () => ["", ["maker", "n", "x"]],
        compile: () => "maker(n)(x)",
        signature: ["function", "number", "number"],
      });
      expect(applyMaker(makeAdder, 5, 3)).toBe(8);
    });

    it("should work with complex combinations", () => {
      const add = bless((a, b) => a + b);
      const mul = bless((a, b) => a * b);
      const complex = concoct({ add, mul }, "(f, g, x, y) => mul(f(x), g(y))", {
        parseFunDefn: () => ["", ["f", "g", "x", "y"]],
        compile: () => "mul(f(x), g(y))",
        signature: ["function", "function", "number", "number"],
      });

      const inc = bless((x) => x + 1);
      const double = bless((x) => x * 2);
      expect(complex(inc, double, 5, 3)).toBe(36); // (5 + 1) * (3 * 2) = 6 * 6
    });

    it("should preserve attributes on passed functions", () => {
      const namedFunc = bless((x) => x + 1, {
        name: "increment",
        signature: ["number"],
      });
      const apply = concoct({}, "(f, x) => f(x)", {
        parseFunDefn: () => ["", ["f", "x"]],
        compile: () => "f(x)",
        signature: ["function", "number"],
      });

      const result = apply(namedFunc, 5);
      expect(result).toBe(6);
      // The passed function should still have its attributes
      expect(namedFunc.concoctAttributes).toEqual({
        name: "increment",
        signature: ["number"],
      });
    });

    it("should handle function parameters with other parameter types", () => {
      const applyWithString = concoct({}, "(f, s, n) => f(n)", {
        parseFunDefn: () => ["", ["f", "s", "n"]],
        compile: () => "f(n)",
        signature: ["function", "string", "number"],
      });

      const square = bless((x) => x * x);
      expect(applyWithString(square, "ignored", 4)).toBe(16);
    });

    it("should work when function parameter is called multiple times", () => {
      const add = bless((a, b) => a + b);
      const callThreeTimes = concoct(
        { add },
        "(f, x) => add(add(f(x), f(x)), f(x))",
        {
          parseFunDefn: () => ["", ["f", "x"]],
          compile: () => "add(add(f(x), f(x)), f(x))",
          signature: ["function", "number"],
        },
      );

      const inc = bless((x) => x + 1);
      expect(callThreeTimes(inc, 5)).toBe(18); // (6 + 6) + 6
    });
  });
});