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 { buildMotionContext, compilePathToPlanRequest, parseProcedureRunPathStatements } from "../../src/grl/semantic/index.js"; const PROGRAM = `language grl 0.1 module Main persistent tool gripper = tool { tcp: pose(0 mm, 0 mm, 100 mm, 0 deg, 0 deg, 0 deg) } persistent frame fixture = frame { origin: pose(800 mm, 0 mm, 0 mm, 0 deg, 0 deg, 0 deg) } const speed v_joint = joint(60 %) const speed v_linear = linear(300 mm/s) const zone z10 = z(10 mm) target home = joint_target { joints: [0 deg, 0 deg] } target pick = pose_target { pose: pose(500 mm, 0 mm, 0 mm, 0 deg, 0 deg, 0 deg) } target mid = pose_target { pose: pose(550 mm, 50 mm, 0 mm, 0 deg, 0 deg, 0 deg) } target arc_end = pose_target { pose: pose(600 mm, 0 mm, 0 mm, 0 deg, 0 deg, 0 deg) } path pick_path { source { type: cad_curve id: "edge_032" sample_distance: 5 mm } defaults { tool: gripper, frame: fixture, speed: v_linear, zone: z10 } point approach movej home speed v_joint zone fine point p1 movel pick offset z 100 mm point p2 movec via mid target arc_end speed linear(150 mm/s) zone fine event before p1 io.do[10] = true event after p2 io.do[10] = false event at p1 distance -20 mm pulse io.do[20] duration 100 ms } proc main() run_path pick_path end end `; function declarations() { return parseGrl(PROGRAM).module.declarations; } function motionContext(decls = declarations()) { return buildMotionContext( decls.filter( (decl): decl is GrlDataDeclaration | GrlTargetDeclaration => decl.kind === "DataDeclaration" || decl.kind === "TargetDeclaration" ) ); } describe("GRL path compilation", () => { it("parses path defaults, source metadata, points, and events as AST nodes", () => { const path = declarations().find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!; expect(path).toMatchObject({ kind: "PathDeclaration", name: "pick_path", items: [ { kind: "PathSourceBlock" }, { kind: "PathDefaultsBlock" }, { kind: "PathPoint", id: "approach" }, { kind: "PathPoint", id: "p1" }, { kind: "PathPoint", id: "p2" }, { kind: "PathEvent", timing: "before", pointId: "p1" }, { kind: "PathEvent", timing: "after", pointId: "p2" }, { kind: "PathEvent", timing: "at", pointId: "p1" } ] }); expect(path.items[0]).toMatchObject({ properties: [ { key: "type", value: { kind: "IdentifierExpression", name: "cad_curve" } }, { key: "id", value: { kind: "StringLiteral", value: "edge_032" } }, { key: "sample_distance", value: { kind: "NumberLiteral" } } ] }); }); it("compiles a path to PathPlanRequest with defaults, source map, source metadata, and events", () => { const decls = declarations(); const path = decls.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!; const compiled = compilePathToPlanRequest(path, motionContext(decls), { startJoints: [0, 0], sampleTime: 0.004 }); expect(compiled.pathId).toBe("pick_path"); expect(compiled.request).toMatchObject({ pathId: "pick_path", startJoints: [0, 0], sampleTime: 0.004, source: { type: "cad_curve", id: "edge_032", sample_distance: 0.005 }, segments: [ { id: "approach", motion: "MOVEJ", targetId: "home", speed: { kind: "joint_percent", value: 0.6 }, zone: { kind: "fine" } }, { id: "p1", motion: "MOVEL", targetId: "pick", speed: { kind: "linear", velocity: 0.3 }, zone: { kind: "distance", value: 0.01 }, tool: { position: [0, 0, 0.1] }, frame: { position: [0.8, 0, 0] }, sourceMap: { line: 37 } }, { id: "p2", motion: "MOVEC", targetId: "arc_end", speed: { kind: "linear", velocity: 0.15 }, zone: { kind: "fine" } } ], events: [ { timing: "before", pointId: "p1", kind: "io", data: { statement: "io . do [ 10 ] = true" } }, { timing: "after", pointId: "p2", kind: "io" }, { timing: "at", pointId: "p1", distance: -0.02, kind: "pulse" } ] }); expect(compiled.request.segments[1]?.target).toMatchObject({ pose: { position: [0.5, 0, 0.1] } }); }); it("extracts run_path statements from procedure body tokens", () => { const procedure = declarations().find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!; expect(parseProcedureRunPathStatements(procedure)).toEqual([ { kind: "RUN_PATH", pathId: "pick_path", sourceMap: { line: 44, column: 5 } } ]); }); it("reports empty paths and duplicate point names", () => { const emptyPath = parseGrl(`language grl 0.1 module Main path empty_path { } end `).module.declarations.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!; expect(() => compilePathToPlanRequest(emptyPath, motionContext([]), { startJoints: [], sampleTime: 0.004 }) ).toThrowError(expect.objectContaining({ code: "GRL_PATH_EMPTY" })); const duplicatePath = parseGrl(`language grl 0.1 module Main const speed v = joint(50 %) const zone zf = fine target home = joint_target { joints: [0 deg] } path dup_path { defaults { speed: v, zone: zf } point p movej home point p movej home } end `).module.declarations.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!; const duplicateContext = motionContext(parseGrl(`language grl 0.1 module Main const speed v = joint(50 %) const zone zf = fine target home = joint_target { joints: [0 deg] } end `).module.declarations); expect(() => compilePathToPlanRequest(duplicatePath, duplicateContext, { startJoints: [0], sampleTime: 0.004 }) ).toThrowError(expect.objectContaining({ code: "GRL_PATH_POINT_DUPLICATE" })); }); it("reports events that reference missing points", () => { const path = parseGrl(`language grl 0.1 module Main const speed v = joint(50 %) const zone zf = fine target home = joint_target { joints: [0 deg] } path bad_event { defaults { speed: v, zone: zf } point p movej home event after missing io.do[1] = true } end `).module.declarations.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!; const context = motionContext(parseGrl(`language grl 0.1 module Main const speed v = joint(50 %) const zone zf = fine target home = joint_target { joints: [0 deg] } end `).module.declarations); expect(() => compilePathToPlanRequest(path, context, { startJoints: [0], sampleTime: 0.004 }) ).toThrowError(expect.objectContaining({ code: "GRL_PATH_EVENT_POINT_NOT_FOUND" })); }); });