// 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
});
});
});