// Copyright (c) 2026 Chris Pressey, Cat's Eye Technologies.
//
// SPDX-License-Identifier: LicenseRef-MIT-X-Jaft
import { describe, it, expect } from "vitest";
import { Scanner, Parser, compile } from "../src/jaftCompiler.js";
describe("Scanner", () => {
describe("basic tokens", () => {
it("should tokenize identifiers", () => {
const t = new Scanner("foo");
expect(t.token).toBe("foo");
});
it("should tokenize parentheses", () => {
const t = new Scanner("()");
expect(t.token).toBe("(");
t.scan();
expect(t.token).toBe(")");
});
it("should tokenize commas", () => {
const t = new Scanner(",");
expect(t.token).toBe(",");
});
it("should skip whitespace", () => {
const t = new Scanner(" foo ");
expect(t.token).toBe("foo");
});
it("should return null at end of input", () => {
const t = new Scanner("x");
t.scan();
expect(t.token).toBe(null);
});
});
describe("numeric literals", () => {
it("should tokenize integers", () => {
const t = new Scanner("42");
expect(t.token).toBe("42");
});
it("should tokenize decimals", () => {
const t = new Scanner("3.14");
expect(t.token).toBe("3.14");
});
});
describe("string literals", () => {
it("should tokenize double-quoted strings", () => {
const t = new Scanner('"hello"');
expect(t.token).toEqual({ type: "string", value: "hello" });
});
it("should tokenize single-quoted strings", () => {
const t = new Scanner("'world'");
expect(t.token).toEqual({ type: "string", value: "world" });
});
it("should handle escaped characters", () => {
const t = new Scanner('"hello\\nworld"');
expect(t.token).toEqual({ type: "string", value: "hello\nworld" });
});
it("should handle escaped quotes", () => {
const t = new Scanner('"say \\"hello\\""');
expect(t.token).toEqual({ type: "string", value: 'say "hello"' });
});
});
describe("boolean literals", () => {
it("should tokenize true", () => {
const t = new Scanner("true");
expect(t.token).toEqual({ type: "boolean", value: true });
});
it("should tokenize false", () => {
const t = new Scanner("false");
expect(t.token).toEqual({ type: "boolean", value: false });
});
});
describe("complex expressions", () => {
it("should tokenize function calls", () => {
const t = new Scanner("add(1, 2)");
expect(t.token).toBe("add");
t.scan();
expect(t.token).toBe("(");
t.scan();
expect(t.token).toBe("1");
t.scan();
expect(t.token).toBe(",");
t.scan();
expect(t.token).toBe("2");
t.scan();
expect(t.token).toBe(")");
});
it("should handle multiple identifiers", () => {
const t = new Scanner("foo bar baz");
expect(t.token).toBe("foo");
t.scan();
expect(t.token).toBe("bar");
t.scan();
expect(t.token).toBe("baz");
});
});
describe("isOn and expect", () => {
it("should check if on a token", () => {
const t = new Scanner("(");
expect(t.isOn("(")).toBe(true);
expect(t.isOn(")")).toBe(false);
});
it("should expect and advance for matching token", () => {
const t = new Scanner("( )");
t.expect("(");
expect(t.token).toBe(")");
});
it("should throw error for non-matching token", () => {
const t = new Scanner("(");
expect(() => t.expect(")")).toThrow("Expected ), found (");
});
});
});
describe("Parser", () => {
describe("parseExpr", () => {
it("should parse variables", () => {
const parser = new Parser(new Scanner("x"));
const ast = parser.parseExpr();
expect(ast).toEqual({ type: "variable", name: "x" });
});
it("should parse numeric literals", () => {
const parser = new Parser(new Scanner("42"));
const ast = parser.parseExpr();
expect(ast).toEqual({ type: "literal", valueType: "number", value: 42 });
});
it("should parse string literals", () => {
const p = new Parser(new Scanner('"hello"'));
const ast = p.parseExpr();
expect(ast).toEqual({
type: "literal",
valueType: "string",
value: "hello",
});
});
it("should parse boolean literals", () => {
const p = new Parser(new Scanner("true"));
const ast = p.parseExpr();
expect(ast).toEqual({
type: "literal",
valueType: "boolean",
value: true,
});
});
it("should parse function calls with no arguments", () => {
const s = new Scanner("random()");
const p = new Parser(s);
const ast = p.parseExpr();
expect(ast).toEqual({
type: "call",
name: "random",
args: [],
});
});
it("should parse function calls with one argument", () => {
const s = new Scanner("sqrt(16)");
const p = new Parser(s);
const ast = p.parseExpr();
expect(ast).toEqual({
type: "call",
name: "sqrt",
args: [{ type: "literal", valueType: "number", value: 16 }],
});
});
it("should parse function calls with multiple arguments", () => {
const t = new Scanner("add(1, 2)");
const p = new Parser(t);
const ast = p.parseExpr();
expect(ast).toEqual({
type: "call",
name: "add",
args: [
{ type: "literal", valueType: "number", value: 1 },
{ type: "literal", valueType: "number", value: 2 },
],
});
});
it("should parse nested function calls", () => {
const t = new Scanner("mul(add(1, 2), 3)");
const p = new Parser(t);
const ast = p.parseExpr();
expect(ast).toEqual({
type: "call",
name: "mul",
args: [
{
type: "call",
name: "add",
args: [
{ type: "literal", valueType: "number", value: 1 },
{ type: "literal", valueType: "number", value: 2 },
],
},
{ type: "literal", valueType: "number", value: 3 },
],
});
});
it("should parse function calls with variables", () => {
const t = new Scanner("mul(a, b)");
const p = new Parser(t);
const ast = p.parseExpr();
expect(ast).toEqual({
type: "call",
name: "mul",
args: [
{ type: "variable", name: "a" },
{ type: "variable", name: "b" },
],
});
});
it("should throw an error for mismatched parentheses", () => {
const p = new Parser(new Scanner('(a) => a === "FOO") ? 1 : 2'));
expect(() => p.parseFunDefn()).toThrow("Unexpected )");
});
it("should throw an error for unmatched parentheses", () => {
const p = new Parser(new Scanner('(a) => a === "FOO" ? 1 : (2'));
expect(() => p.parseFunDefn()).toThrow("Expected )");
});
});
describe("parseExpr with binops", () => {
it("should parse arithmetic operators with correct precedence", () => {
const t = new Scanner("a + b * c");
const p = new Parser(t);
const ast = p.parseExpr();
expect(ast).toEqual({
type: "binop",
op: "+",
left: { type: "variable", name: "a" },
right: {
type: "binop",
op: "*",
left: { type: "variable", name: "b" },
right: { type: "variable", name: "c" },
},
});
});
it("should parse comparison with arithmetic", () => {
const t = new Scanner("x + 1 >= y * 2");
const ast = new Parser(t).parseExpr();
expect(ast.type).toBe("binop");
expect(ast.op).toBe(">=");
expect(ast.left.type).toBe("binop");
expect(ast.left.op).toBe("+");
expect(ast.right.type).toBe("binop");
expect(ast.right.op).toBe("*");
});
it("should parse comparison with logical operators", () => {
const t = new Scanner("x >= 1 && y <= 2");
const ast = new Parser(t).parseExpr();
expect(ast.type).toBe("binop");
expect(ast.op).toBe("&&");
expect(ast.left.type).toBe("binop");
expect(ast.left.op).toBe(">=");
expect(ast.right.type).toBe("binop");
expect(ast.right.op).toBe("<=");
});
it("should parse member access with highest precedence", () => {
const t = new Scanner("obj.prop + 5");
const ast = new Parser(t).parseExpr();
expect(ast).toEqual({
type: "binop",
op: "+",
left: {
type: "binop",
op: ".",
left: { type: "variable", name: "obj" },
right: { type: "literal", valueType: "string", value: "prop" },
},
right: { type: "literal", valueType: "number", value: 5 },
});
});
});
describe("parseExpr with let", () => {
it("should parse simple let expression", () => {
const t = new Scanner("let x = 5; x");
const ast = new Parser(t).parseExpr();
expect(ast).toEqual({
type: "let",
allowShadowing: true,
bindings: [
{
name: "x",
value: { type: "literal", valueType: "number", value: 5 },
},
],
body: { type: "variable", name: "x" },
});
});
it("should parse let with multiple bindings", () => {
const t = new Scanner("let x = 1, y = 2; add(x, y)");
const ast = new Parser(t).parseExpr();
expect(ast).toEqual({
type: "let",
allowShadowing: true,
bindings: [
{
name: "x",
value: { type: "literal", valueType: "number", value: 1 },
},
{
name: "y",
value: { type: "literal", valueType: "number", value: 2 },
},
],
body: {
type: "call",
name: "add",
args: [
{ type: "variable", name: "x" },
{ type: "variable", name: "y" },
],
},
});
});
it("should parse let with function call bindings", () => {
const t = new Scanner("let x = add(1, 2); mul(x, x)");
const ast = new Parser(t).parseExpr();
expect(ast.type).toBe("let");
expect(ast.bindings[0].value.type).toBe("call");
expect(ast.body.type).toBe("call");
});
it("should parse let with ternary bindings and expression", () => {
const t = new Scanner("let x = y > 2 ? 0 : 1; x > 2 ? 0 : 1");
const ast = new Parser(t).parseExpr();
expect(ast.type).toBe("let");
expect(ast.bindings[0].value.type).toBe("ternary");
expect(ast.body.type).toBe("ternary");
});
});
describe("parseExpr with ternary", () => {
it("should parse simple ternary with literals", () => {
const t = new Scanner("true ? 1 : 2");
const ast = new Parser(t).parseExpr();
expect(ast).toEqual({
type: "ternary",
condition: { type: "literal", valueType: "boolean", value: true },
consequent: { type: "literal", valueType: "number", value: 1 },
alternate: { type: "literal", valueType: "number", value: 2 },
});
});
it("should parse ternary with variables", () => {
const t = new Scanner("x ? y : z");
const ast = new Parser(t).parseExpr();
expect(ast).toEqual({
type: "ternary",
condition: { type: "variable", name: "x" },
consequent: { type: "variable", name: "y" },
alternate: { type: "variable", name: "z" },
});
});
it("should parse ternary with function calls", () => {
const t = new Scanner("isPositive(x) ? add(x, 1) : sub(x, 1)");
const ast = new Parser(t).parseExpr();
expect(ast).toEqual({
type: "ternary",
condition: {
type: "call",
name: "isPositive",
args: [{ type: "variable", name: "x" }],
},
consequent: {
type: "call",
name: "add",
args: [
{ type: "variable", name: "x" },
{ type: "literal", valueType: "number", value: 1 },
],
},
alternate: {
type: "call",
name: "sub",
args: [
{ type: "variable", name: "x" },
{ type: "literal", valueType: "number", value: 1 },
],
},
});
});
it("should parse nested ternary expressions", () => {
const t = new Scanner("x ? (y ? 1 : 2) : 3");
const ast = new Parser(t).parseExpr();
expect(ast.type).toBe("ternary");
expect(ast.consequent.type).toBe("ternary");
});
});
});
describe("Compiler", () => {
describe("compile", () => {
it("should compile variables", () => {
const ctx = new Map();
const ast = { type: "variable", name: "x" };
const code = compile(ctx, ast);
expect(code).toBe("x");
});
it("should compile numeric literals", () => {
const ctx = new Map();
const ast = { type: "literal", valueType: "number", value: 42 };
const code = compile(ctx, ast);
expect(code).toBe("42");
});
it("should compile string literals", () => {
const ctx = new Map();
const ast = { type: "literal", valueType: "string", value: "hello" };
const code = compile(ctx, ast);
expect(code).toBe('"hello"');
});
it("should compile boolean literals", () => {
const ctx = new Map();
const ast = { type: "literal", valueType: "boolean", value: true };
const code = compile(ctx, ast);
expect(code).toBe("true");
});
it("should compile function calls", () => {
const add = (a, b) => a + b;
const ctx = new Map([["add", add]]);
const ast = {
type: "call",
name: "add",
args: [
{ type: "literal", valueType: "number", value: 1 },
{ type: "literal", valueType: "number", value: 2 },
],
};
const code = compile(ctx, ast);
expect(code).toBe("add(1, 2)");
});
it("should throw error for unknown function", () => {
const ctx = new Map();
const ast = { type: "call", name: "unknown", args: [] };
expect(() => compile(ctx, ast)).toThrow("Unknown function: unknown");
});
});
describe("compile with binops", () => {
it("should compile arithmetic with parentheses", () => {
const ctx = new Map();
const ast = {
type: "binop",
op: "+",
left: { type: "variable", name: "a" },
right: {
type: "binop",
op: "*",
left: { type: "variable", name: "b" },
right: { type: "literal", valueType: "number", value: 2 },
},
};
const code = compile(ctx, ast);
expect(code).toBe("(a + (b * 2))");
});
it("should compile member access", () => {
const ctx = new Map();
const ast = {
type: "binop",
op: ".",
left: { type: "variable", name: "obj" },
right: { type: "literal", valueType: "string", value: "length" },
};
const code = compile(ctx, ast);
expect(code).toBe("(obj.length)");
});
});
describe("compile with let", () => {
it("should compile let expressions", () => {
const ctx = new Map();
const ast = {
type: "let",
bindings: [
{
name: "x",
value: { type: "literal", valueType: "number", value: 5 },
},
],
body: { type: "variable", name: "x" },
};
const code = compile(ctx, ast);
expect(code).toBe("((x) => x)(5)");
});
it("should compile let with multiple bindings", () => {
const add = (a, b) => a + b;
const ctx = new Map([["add", add]]);
const ast = {
type: "let",
bindings: [
{
name: "x",
value: { type: "literal", valueType: "number", value: 1 },
},
{
name: "y",
value: { type: "literal", valueType: "number", value: 2 },
},
],
body: {
type: "call",
name: "add",
args: [
{ type: "variable", name: "x" },
{ type: "variable", name: "y" },
],
},
};
const code = compile(ctx, ast);
expect(code).toContain("(x, y) =>");
});
});
describe("compile with ternary", () => {
it("should compile ternary expressions", () => {
const ctx = new Map();
const ast = {
type: "ternary",
condition: { type: "literal", valueType: "boolean", value: true },
consequent: { type: "literal", valueType: "number", value: 1 },
alternate: { type: "literal", valueType: "number", value: 2 },
};
const code = compile(ctx, ast);
expect(code).toBe("(true ? 1 : 2)");
});
});
});