import { describe, expect, it } from "vitest"; import { GRL_KEYWORDS, lexGrl, normalizeUnitLiteral, normalizeUnitValue } from "../../src/grl/lexer/index.js"; import type { GrlNumberToken } from "../../src/grl/lexer/index.js"; function numbers(source: string): GrlNumberToken[] { return lexGrl(source).filter((token): token is GrlNumberToken => token.kind === "number"); } describe("GRL lexer", () => { it("recognizes comments, keywords, identifiers, and source positions", () => { const tokens = lexGrl(`// generated\nlanguage grl 0.1\nmodule Main\n proc main()\n end\nend\n`); expect(tokens[0]).toMatchObject({ kind: "comment", style: "line", value: " generated", range: { start: { line: 1, column: 1 }, end: { line: 1, column: 13 } } }); expect(tokens.filter((token) => token.kind === "keyword").map((token) => token.raw)).toEqual([ "language", "module", "proc", "end", "end" ]); expect(tokens.find((token) => token.raw === "Main")).toMatchObject({ kind: "identifier", range: { start: { line: 3, column: 8 } } }); expect(tokens.at(-1)).toMatchObject({ kind: "eof" }); }); it("normalizes numeric literals with GRL units into SI values", () => { const found = numbers("100 mm 0.25 m 180 deg 3.14159 rad 300 mm/s 50 % 200 ms 2.5 kg 500 mm/s2"); expect(found.map((token) => token.unit?.raw)).toEqual([ "mm", "m", "deg", "rad", "mm/s", "%", "ms", "kg", "mm/s2" ]); expect(found[0]?.unit?.normalizedValue).toBeCloseTo(0.1); expect(found[1]?.unit?.normalizedValue).toBeCloseTo(0.25); expect(found[2]?.unit?.normalizedValue).toBeCloseTo(Math.PI); expect(found[3]?.unit?.normalizedValue).toBeCloseTo(3.14159); expect(found[4]?.unit?.normalizedValue).toBeCloseTo(0.3); expect(found[5]?.unit?.normalizedValue).toBeCloseTo(0.5); expect(found[6]?.unit?.normalizedValue).toBeCloseTo(0.2); expect(found[7]?.unit?.normalizedValue).toBeCloseTo(2.5); expect(found[8]?.unit?.normalizedValue).toBeCloseTo(0.5); }); it("keeps unit raw text on number tokens", () => { const [token] = numbers("linear(300 mm/s)"); expect(token).toMatchObject({ kind: "number", raw: "300 mm/s", value: 300, unit: { raw: "mm/s", kind: "linear_velocity", siUnit: "m/s" } }); }); it("exposes the full reserved keyword set from the specification", () => { expect(GRL_KEYWORDS).toContain("movej"); expect(GRL_KEYWORDS).toContain("run_operation"); expect(GRL_KEYWORDS).toContain("post_hint"); expect(GRL_KEYWORDS).toContain("continuous"); expect(GRL_KEYWORDS).toHaveLength(85); }); it("provides direct unit helpers for parser and semantic layers", () => { expect(normalizeUnitLiteral("deg/s")).toMatchObject({ kind: "angular_velocity", siUnit: "rad/s" }); expect(normalizeUnitValue(90, "deg/s")).toBeCloseTo(Math.PI / 2); expect(() => normalizeUnitLiteral("inch")).toThrow("Unknown GRL unit"); }); });