import { describe, expect, it } from "vitest"; import type { GrlDataDeclaration, GrlOperationDeclaration, GrlPathDeclaration, GrlProcedureDeclaration, GrlTargetDeclaration } from "../../src/grl/ast/index.js"; import type { PathEventInstruction } from "../../src/grl/ir/index.js"; import { parseGrl } from "../../src/grl/parser/index.js"; import { buildMotionContext, compileOperation, compileOperationActionIo, compilePathEventIo, compilePathToPlanRequest, parseIoFlowStatements, type IoMap } from "../../src/grl/semantic/index.js"; const PROGRAM = `language grl 0.1 module Main const speed v = joint(50 %) const zone zf = fine target home = joint_target { joints: [0 deg] } path io_path { defaults { speed: v, zone: zf } point p0 movej home event at p0 distance 0 mm pulse io.do[20] duration 100 ms } operation io_op { kind: handling path: io_path start_action: wait io.di[4] == true timeout 500 ms on_timeout alarm "part missing" end_action: pulse io.do[5] duration 250 ms } proc main() io.do[1] = true io.go[2] = 16 io.alias.grip_close = false wait all(io.di[1] == true, io.di[2] == false) timeout 2 s on_timeout alarm "Clamp close timeout" wait any(rising(io.di[3]), falling(io.di[4]), changed(io.ai[1])) wait io.di[5] == true timeout 1 s on_timeout call recover pulse io.do[3] duration 200 ms end end `; const IO_MAP: IoMap = { aliases: { grip_close: { domain: "do", index: 6, raw: "io.do[6]" } }, allowedRanges: { ai: { min: 1, max: 8 }, di: { min: 1, max: 16 }, do: { min: 1, max: 32 }, go: { min: 1, max: 4 } } }; 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" ) ); } function pathsByName(paths: GrlPathDeclaration[]) { return new Map(paths.map((path) => [path.name, path])); } describe("GRL IO, wait, and pulse compilation", () => { it("compiles procedure IO writes, wait conditions, timeout actions, and pulse traces", () => { const procedure = declarations().find( (decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration" )!; const instructions = parseIoFlowStatements(procedure.bodyTokens, IO_MAP); expect(instructions).toHaveLength(7); expect(instructions[0]).toMatchObject({ kind: "IO_WRITE", target: { domain: "do", index: 1, raw: "io.do[1]" }, value: true }); expect(instructions[1]).toMatchObject({ kind: "IO_WRITE", target: { domain: "go", index: 2, raw: "io.go[2]" }, value: 16 }); expect(instructions[2]).toMatchObject({ kind: "IO_WRITE", target: { domain: "do", index: 6, raw: "io.do[6]" }, value: false }); expect(instructions[3]).toMatchObject({ kind: "WAIT", condition: "all ( io . di [ 1 ] == true , io . di [ 2 ] == false )", timeout: 2, onTimeout: { kind: "alarm", value: "Clamp close timeout" } }); expect(instructions[4]).toMatchObject({ kind: "WAIT", condition: "any ( rising ( io . di [ 3 ] ) , falling ( io . di [ 4 ] ) , changed ( io . ai [ 1 ] ) )" }); expect(instructions[5]).toMatchObject({ kind: "WAIT", condition: "io . di [ 5 ] == true", timeout: 1, onTimeout: { kind: "call", value: "recover" } }); expect(instructions[6]).toMatchObject({ kind: "PULSE", target: { domain: "do", index: 3, raw: "io.do[3]" }, duration: 0.2, trace: [ { time: 0, action: "set", target: { domain: "do", index: 3 }, value: true }, { time: 0.2, action: "reset", target: { domain: "do", index: 3 }, value: false } ] }); }); it("validates IO addresses against configured ranges", () => { const procedure = parseGrl(`language grl 0.1 module Main proc main() io.do[99] = true end end `).module.declarations.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!; expect(() => parseIoFlowStatements(procedure.bodyTokens, { allowedRanges: { do: { min: 1, max: 16 } } }) ).toThrowError(expect.objectContaining({ code: "GRL_IO_ADDRESS_NOT_FOUND" })); }); it("expands path event IO metadata into pulse IR without entering KDL motion segments", () => { const decls = declarations(); const path = decls.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!; const compiled = compilePathToPlanRequest(path, motionContext(decls), { startJoints: [0], sampleTime: 0.004 }); expect(compiled.request.segments).toHaveLength(1); expect(compiled.request.events).toHaveLength(1); expect(compilePathEventIo(compiled.events[0]!, IO_MAP)).toEqual([ expect.objectContaining({ kind: "PULSE", target: { domain: "do", index: 20, raw: "io.do[20]" }, duration: 0.1 }) ]); }); it("expands operation action metadata into wait and pulse IR with preserved units", () => { const decls = declarations(); const operation = decls.find( (decl): decl is GrlOperationDeclaration => decl.kind === "OperationDeclaration" )!; const paths = decls.filter((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration"); const compiled = compileOperation(operation, pathsByName(paths)); expect(compileOperationActionIo(compiled.startActions[0]!, IO_MAP)).toEqual([ expect.objectContaining({ kind: "WAIT", condition: "io . di [ 4 ] == true", timeout: 0.5, onTimeout: { kind: "alarm", value: "part missing" } }) ]); expect(compileOperationActionIo(compiled.endActions[0]!, IO_MAP)).toEqual([ expect.objectContaining({ kind: "PULSE", target: { domain: "do", index: 5, raw: "io.do[5]" }, duration: 0.25 }) ]); }); it("lexes statement fallback metadata so unit literals and booleans remain typed", () => { const event: PathEventInstruction = { timing: "at", pointId: "p0", kind: "pulse", data: { statement: "pulse io.do[7] duration 125 ms" } }; expect(compilePathEventIo(event, IO_MAP)).toEqual([ expect.objectContaining({ kind: "PULSE", target: { domain: "do", index: 7, raw: "io.do[7]" }, duration: 0.125 }) ]); }); });