import { describe, expect, it } from "vitest"; import type { GrlDataDeclaration, GrlPathDeclaration, GrlProcedureDeclaration, GrlTargetDeclaration } from "../../src/grl/ast/index.js"; import { parseGrl } from "../../src/grl/parser/index.js"; import { parseGrlExpression } from "../../src/grl/parser/expressionParser.js"; import { buildMotionContext, compileGrlDataDeclaration, compileGrlTargetDeclaration, compilePathToPlanRequest, parseIoFlowStatements } from "../../src/grl/semantic/index.js"; import { evaluateNumberExpression } from "../../src/grl/semantic/constantExpression.js"; import { lexGrl } from "../../src/grl/lexer/index.js"; function expression(source: string) { return parseGrlExpression(lexGrl(source, { preserveComments: false }).filter((token) => token.kind !== "eof")); } describe("GRL expression arithmetic and constant folding", () => { it("parses arithmetic, comparison, and logical precedence into AST nodes", () => { expect(expression("1 + 2 * 3")).toMatchObject({ kind: "BinaryExpression", operator: "+", right: { kind: "BinaryExpression", operator: "*" } }); expect(expression("(1 + 2) * 3")).toMatchObject({ kind: "BinaryExpression", operator: "*", left: { kind: "BinaryExpression", operator: "+" } }); expect(expression("not (a == b) or c != d")).toMatchObject({ kind: "BinaryExpression", operator: "or", left: { kind: "UnaryExpression", operator: "not" } }); }); it("evaluates math functions, trigonometry, units, and diagnostics", () => { expect(evaluateNumberExpression(expression("100 + 50 mm/s"), { expectedKind: "linear_velocity", defaultUnit: "mm/s" })).toBeCloseTo(0.15); expect(evaluateNumberExpression(expression("(10 + 5) deg"), { expectedKind: "angle", defaultUnit: "deg" })).toBeCloseTo(Math.PI / 12); expect(evaluateNumberExpression(expression("clamp(20 mm, 1 mm, 10 mm)"), { expectedKind: "length", defaultUnit: "mm" })).toBeCloseTo(0.01); }); it("reports stable expression errors", () => { expect(() => evaluateNumberExpression(expression("sqrt(-1)"))).toThrowError(expect.objectContaining({ code: "GRL_EXPR_DOMAIN" })); expect(() => evaluateNumberExpression(expression("10 mm + 2 s"))).toThrowError(expect.objectContaining({ code: "GRL_EXPR_UNIT_MISMATCH" })); expect(() => evaluateNumberExpression(expression("1 / 0"))).toThrowError(expect.objectContaining({ code: "GRL_EXPR_DIV_ZERO" })); }); it("folds speed, zone, target, path event, wait, and pulse expressions", () => { const program = parseGrl(`language grl 0.1 module MathMotion const num blend_base = 5 + 5 const speed v_pick = linear(100 + 50 mm/s) const speed v_safe = linear(max(50 mm/s, 200 mm/s / 2)) const zone z_app = z(clamp(blend_base mm, 1 mm, 50 mm)) target home = joint_target { joints: [0 deg, (10 + 5) deg, -90 deg] } target pick = pose_target { pose: pose(400 + 50 mm, 20 * 2 mm, sqrt(90000) mm, 0 deg, 0 deg, atan2(1, 1)) } path main_path { defaults { speed: v_pick, zone: z_app } point p0 movej home speed linear(100 + 50 mm/s) zone z(5 + 5 mm) event at p0 distance 5 + 5 mm pulse io.do[1] duration 50 + 50 ms } proc main() wait io.di[1] == true timeout 1 + 1 s pulse io.do[2] duration 50 + 50 ms end end `); const declarations = program.module.declarations; const [blendBase, vPick, vSafe] = declarations.filter( (decl): decl is GrlDataDeclaration => decl.kind === "DataDeclaration" ); expect(compileGrlDataDeclaration(blendBase!)).toMatchObject({ value: 10 }); expect(compileGrlDataDeclaration(vPick!).value).toMatchObject({ kind: "linear" }); expect((compileGrlDataDeclaration(vPick!).value as { velocity: number }).velocity).toBeCloseTo(0.15); expect(compileGrlDataDeclaration(vSafe!).value).toMatchObject({ kind: "linear" }); expect((compileGrlDataDeclaration(vSafe!).value as { velocity: number }).velocity).toBeCloseTo(0.1); const [home, pick] = declarations.filter((decl): decl is GrlTargetDeclaration => decl.kind === "TargetDeclaration"); expect(compileGrlTargetDeclaration(home!)).toMatchObject({ target: { joints: [0, Math.PI / 12, -Math.PI / 2] } }); const pickTarget = compileGrlTargetDeclaration(pick!); expect("pose" in pickTarget.target && pickTarget.target.pose.position).toEqual([0.45, 0.04, 0.3]); const context = buildMotionContext( declarations.filter( (decl): decl is GrlDataDeclaration | GrlTargetDeclaration => decl.kind === "DataDeclaration" || decl.kind === "TargetDeclaration" ) ); const path = declarations.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!; const compiledPath = compilePathToPlanRequest(path, context, { startJoints: [0, 0, 0], sampleTime: 0.004 }); expect(compiledPath.request.segments[0]).toMatchObject({ speed: { kind: "linear" }, zone: { kind: "distance", value: 0.01 } }); expect((compiledPath.request.segments[0]?.speed as { velocity: number }).velocity).toBeCloseTo(0.15); expect(compiledPath.events[0]).toMatchObject({ distance: 0.01 }); const procedure = declarations.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!; expect(parseIoFlowStatements(procedure.bodyTokens)).toEqual([ expect.objectContaining({ kind: "WAIT", timeout: 2 }), expect.objectContaining({ kind: "PULSE", duration: 0.1 }) ]); }); });