同步KDL工程源码到云仓库

This commit is contained in:
wangdequan
2026-06-27 08:45:38 -04:00
parent 93d8ede54b
commit 95c684fc4d
93 changed files with 25712 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
import { describe, expect, it } from "vitest";
import type { GrlProcedureDeclaration } from "../../src/grl/ast/index.js";
import { parseGrl } from "../../src/grl/parser/index.js";
import { parseProcedureControlFlow } from "../../src/grl/semantic/index.js";
function procedure(source: string): GrlProcedureDeclaration {
return parseGrl(source).module.declarations.find(
(decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration"
)!;
}
describe("GRL control-flow compilation", () => {
it("compiles if, elseif, else, while, for, switch, labels, and jumps", () => {
const proc = procedure(`language grl 0.1
module Main
proc main()
label retry
if ready == true
wait io.di[1] == true
elseif fault == true
jump recovery
else
jump retry
end
while all(io.di[1] == true, io.di[2] == false)
continue
end
for i = 1 to 3 step 1
movej home
end
switch mode
case 1
break
case 2
jump done
default
jump recovery
end
label recovery
label done
end
end
`);
const flow = parseProcedureControlFlow(proc);
expect(flow).toEqual([
expect.objectContaining({ kind: "LABEL", name: "retry", scopePath: [] }),
expect.objectContaining({
kind: "IF",
branches: [
expect.objectContaining({
branchKind: "if",
condition: expect.objectContaining({ text: "ready == true" }),
body: [expect.objectContaining({ kind: "RAW_STATEMENT", text: "wait io . di [ 1 ] == true" })]
}),
expect.objectContaining({
branchKind: "elseif",
condition: expect.objectContaining({ text: "fault == true" }),
body: [expect.objectContaining({ kind: "JUMP", label: "recovery" })]
}),
expect.objectContaining({
branchKind: "else",
body: [expect.objectContaining({ kind: "JUMP", label: "retry" })]
})
]
}),
expect.objectContaining({
kind: "WHILE",
condition: expect.objectContaining({
text: "all ( io . di [ 1 ] == true , io . di [ 2 ] == false )"
}),
body: [expect.objectContaining({ kind: "CONTINUE" })]
}),
expect.objectContaining({
kind: "FOR",
iterator: "i",
from: expect.objectContaining({ text: "1" }),
to: expect.objectContaining({ text: "3" }),
step: expect.objectContaining({ text: "1" }),
body: [expect.objectContaining({ kind: "RAW_STATEMENT", text: "movej home" })]
}),
expect.objectContaining({
kind: "SWITCH",
expression: expect.objectContaining({ text: "mode" }),
cases: [
expect.objectContaining({ caseKind: "case", value: 1, body: [expect.objectContaining({ kind: "BREAK" })] }),
expect.objectContaining({ caseKind: "case", value: 2, body: [expect.objectContaining({ kind: "JUMP", label: "done" })] }),
expect.objectContaining({ caseKind: "default", body: [expect.objectContaining({ kind: "JUMP", label: "recovery" })] })
]
}),
expect.objectContaining({ kind: "LABEL", name: "recovery", scopePath: [] }),
expect.objectContaining({ kind: "LABEL", name: "done", scopePath: [] })
]);
});
it("reports non-boolean control conditions", () => {
const proc = procedure(`language grl 0.1
module Main
proc main()
if 1
end
end
end
`);
expect(() => parseProcedureControlFlow(proc)).toThrowError(
expect.objectContaining({ code: "GRL_CONTROL_CONDITION_NOT_BOOL" })
);
});
it("reports break and continue outside valid blocks", () => {
const breakProc = procedure(`language grl 0.1
module Main
proc main()
break
end
end
`);
const continueProc = procedure(`language grl 0.1
module Main
proc main()
switch mode
case 1
continue
end
end
end
`);
expect(() => parseProcedureControlFlow(breakProc)).toThrowError(
expect.objectContaining({ code: "GRL_BREAK_OUTSIDE_FLOW" })
);
expect(() => parseProcedureControlFlow(continueProc)).toThrowError(
expect.objectContaining({ code: "GRL_CONTINUE_OUTSIDE_LOOP" })
);
});
it("reports duplicate or non-constant switch cases", () => {
const duplicateProc = procedure(`language grl 0.1
module Main
proc main()
switch mode
case 1
break
case 1
break
end
end
end
`);
const nonConstantProc = procedure(`language grl 0.1
module Main
proc main()
switch mode
case mode + 1
break
end
end
end
`);
expect(() => parseProcedureControlFlow(duplicateProc)).toThrowError(
expect.objectContaining({ code: "GRL_SWITCH_CASE_DUPLICATE" })
);
expect(() => parseProcedureControlFlow(nonConstantProc)).toThrowError(
expect.objectContaining({ code: "GRL_SWITCH_CASE_NOT_CONSTANT" })
);
});
it("reports labels that cannot be reached by jump", () => {
const intoBlockProc = procedure(`language grl 0.1
module Main
proc main()
jump inner
if ready == true
label inner
end
end
end
`);
const missingLabelProc = procedure(`language grl 0.1
module Main
proc main()
jump missing
end
end
`);
expect(() => parseProcedureControlFlow(intoBlockProc)).toThrowError(
expect.objectContaining({ code: "GRL_JUMP_INTO_BLOCK" })
);
expect(() => parseProcedureControlFlow(missingLabelProc)).toThrowError(
expect.objectContaining({ code: "GRL_LABEL_NOT_FOUND" })
);
});
});

View File

@@ -0,0 +1,151 @@
import { describe, expect, it } from "vitest";
import type { GrlDataDeclaration, GrlTargetDeclaration } from "../../src/grl/ast/index.js";
import { parseGrl } from "../../src/grl/parser/index.js";
import {
compileGrlDataDeclaration,
compileGrlTargetDeclaration,
compileOffsetExpression
} from "../../src/grl/semantic/index.js";
function dataDeclarations(source: string): GrlDataDeclaration[] {
return parseGrl(source).module.declarations.filter(
(declaration): declaration is GrlDataDeclaration => declaration.kind === "DataDeclaration"
);
}
function targetDeclarations(source: string): GrlTargetDeclaration[] {
return parseGrl(source).module.declarations.filter(
(declaration): declaration is GrlTargetDeclaration => declaration.kind === "TargetDeclaration"
);
}
describe("GRL data declarations and target compilation", () => {
it("compiles tool and frame declarations into shared structures", () => {
const [toolDecl, frameDecl] = dataDeclarations(`language grl 0.1
module Main
persistent tool gripper = tool {
tcp: pose(0 mm, 0 mm, 180 mm, 0 deg, 0 deg, 0 deg),
mass: 2.5 kg,
cog: [0 mm, 0 mm, 80 mm]
}
persistent frame fixture = frame {
origin: pose(800 mm, 0 mm, 200 mm, 0 deg, 0 deg, 0 deg)
}
end
`);
expect(toolDecl).toMatchObject({
storage: "persistent",
typeName: "tool",
name: "gripper",
initializer: { kind: "ObjectExpression", typeName: "tool" }
});
expect(compileGrlDataDeclaration(toolDecl!)).toMatchObject({
name: "gripper",
value: {
tcp: {
position: [0, 0, 0.18],
quaternion: [0, 0, 0, 1]
},
mass: 2.5,
cog: [0, 0, 0.08]
}
});
expect(compileGrlDataDeclaration(frameDecl!)).toMatchObject({
name: "fixture",
value: {
origin: {
position: [0.8, 0, 0.2],
quaternion: [0, 0, 0, 1]
}
}
});
});
it("compiles speed and zone declarations", () => {
const declarations = dataDeclarations(`language grl 0.1
module Main
const speed v_joint = joint(80 %)
const speed v_pick = linear(300 mm/s)
const speed v_slow = linear(100 mm/s, acc 500 mm/s2)
const zone z_fine = fine
const zone z10 = z(10 mm)
const zone z_cnt = cnt(30)
const zone z_cont = continuous
end
`);
const compiled = declarations.map(compileGrlDataDeclaration);
expect(compiled).toMatchObject([
{ name: "v_joint", value: { kind: "joint_percent", value: 0.8 } },
{ name: "v_pick", value: { kind: "linear", velocity: 0.3 } },
{ name: "v_slow", value: { kind: "linear", velocity: 0.1, acceleration: 0.5 } },
{ name: "z_fine", value: { kind: "fine" } },
{ name: "z10", value: { kind: "distance", value: 0.01 } },
{ name: "z_cnt", value: { kind: "cnt", value: 30 } },
{ name: "z_cont", value: { kind: "continuous" } }
]);
});
it("compiles joint_target and pose_target declarations", () => {
const [home, pick] = targetDeclarations(`language grl 0.1
module Main
target home = joint_target {
joints: [0 deg, -30 deg, 60 deg, 0 deg, 60 deg, 0 deg]
}
target pick = pose_target {
pose: pose(500 mm, 120 mm, 300 mm, 180 deg, 0 deg, 90 deg),
config: robot_config(0, 0, 1),
tool: gripper,
frame: fixture
}
end
`);
expect(compileGrlTargetDeclaration(home!)).toMatchObject({
name: "home",
target: {
joints: [0, -Math.PI / 6, Math.PI / 3, 0, Math.PI / 3, 0]
}
});
const compiledPick = compileGrlTargetDeclaration(pick!);
expect(compiledPick.name).toBe("pick");
expect("pose" in compiledPick.target).toBe(true);
if ("pose" in compiledPick.target) {
expect(compiledPick.target.pose.position).toEqual([0.5, 0.12, 0.3]);
expect(compiledPick.target.config).toEqual({ shoulder: 0, elbow: 0, wrist: 1 });
}
});
it("parses and compiles offset expressions", () => {
const [declFrame, declTool] = dataDeclarations(`language grl 0.1
module Main
var pose_target p2 = pick offset x 20 mm y -10 mm z 50 mm
var pose_target p3 = pick offset_in tool z -50 mm
end
`);
expect(declFrame?.initializer).toMatchObject({
kind: "OffsetExpression",
mode: "frame",
axes: [
{ axis: "x" },
{ axis: "y" },
{ axis: "z" }
]
});
if (declFrame?.initializer.kind === "OffsetExpression") {
expect(compileOffsetExpression(declFrame.initializer)).toEqual({
mode: "frame",
xyz: [0.02, -0.01, 0.05]
});
}
if (declTool?.initializer.kind === "OffsetExpression") {
expect(compileOffsetExpression(declTool.initializer)).toEqual({
mode: "tool",
xyz: [0, 0, -0.05]
});
}
});
});

View File

@@ -0,0 +1,125 @@
import { describe, expect, it } from "vitest";
import type { GrlProcedureDeclaration, GrlRawTopLevelDeclaration } from "../../src/grl/ast/index.js";
import { parseGrl } from "../../src/grl/parser/index.js";
import {
analyzeExceptionSemantics,
parseProcedureExceptionFlow
} from "../../src/grl/semantic/index.js";
const PROGRAM = `language grl 0.1
module Main
trap recover_trap()
raise E_STOP
end
task background cycle 10 ms
call monitor()
end
proc main()
alarm E_STOP "Emergency stop" severity fatal
try
raise E_STOP
catch E_STOP
alarm RECOVER "Recovering" severity warning
finally
alarm CLEANUP "Cleanup"
end
enable interrupt guard
disable interrupt guard
end
end
`;
function declarations() {
return parseGrl(PROGRAM).module.declarations;
}
describe("GRL alarm, raise, try/catch, interrupt, and task semantics", () => {
it("keeps trap and task as parsed raw declarations for P1 diagnostics", () => {
const raw = declarations().filter(
(decl): decl is GrlRawTopLevelDeclaration => decl.kind === "RawTopLevelDeclaration"
);
expect(raw[0]?.declarationType).toBe("trap");
expect(raw[0]?.tokens[0]).toMatchObject({ raw: "trap" });
expect(raw[0]?.tokens[1]).toMatchObject({ raw: "recover_trap" });
expect(raw[1]?.declarationType).toBe("task");
expect(raw[1]?.tokens[0]).toMatchObject({ raw: "task" });
expect(raw[1]?.tokens[1]).toMatchObject({ raw: "background" });
});
it("compiles alarm, raise, try/catch/finally, and interrupt diagnostics from procedure body", () => {
const procedure = declarations().find(
(decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration"
)!;
expect(parseProcedureExceptionFlow(procedure)).toEqual([
expect.objectContaining({
kind: "ALARM",
alarmId: "E_STOP",
message: "Emergency stop",
severity: "fatal"
}),
expect.objectContaining({
kind: "TRY",
body: [expect.objectContaining({ kind: "RAISE", alarmId: "E_STOP" })],
catches: [
expect.objectContaining({
alarmId: "E_STOP",
body: [
expect.objectContaining({
kind: "ALARM",
alarmId: "RECOVER",
message: "Recovering",
severity: "warning"
})
]
})
],
finally: expect.objectContaining({
body: [expect.objectContaining({ kind: "ALARM", alarmId: "CLEANUP", message: "Cleanup" })]
})
}),
expect.objectContaining({ kind: "UNSUPPORTED_RUNTIME", feature: "interrupt" }),
expect.objectContaining({ kind: "UNSUPPORTED_RUNTIME", feature: "interrupt" })
]);
});
it("reports P1 trap/task semantics as explicit unsupported diagnostics", () => {
const analysis = analyzeExceptionSemantics(declarations());
expect(analysis.unsupported).toEqual([
expect.objectContaining({ kind: "UNSUPPORTED_RUNTIME", feature: "trap" }),
expect.objectContaining({ kind: "UNSUPPORTED_RUNTIME", feature: "task" })
]);
expect(analysis.diagnostics).toEqual([
expect.objectContaining({ severity: "warning", code: "GRL_P1_UNIMPLEMENTED" }),
expect.objectContaining({ severity: "warning", code: "GRL_P1_UNIMPLEMENTED" })
]);
});
it("reports missing alarm ids and try blocks without handlers", () => {
const missingAlarmId = parseGrl(`language grl 0.1
module Main
proc main()
alarm
end
end
`).module.declarations.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!;
const tryWithoutHandler = parseGrl(`language grl 0.1
module Main
proc main()
try
raise E_STOP
end
end
end
`).module.declarations.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!;
expect(() => parseProcedureExceptionFlow(missingAlarmId)).toThrowError(
expect.objectContaining({ code: "GRL_ALARM_ID_MISSING" })
);
expect(() => parseProcedureExceptionFlow(tryWithoutHandler)).toThrowError(
expect.objectContaining({ code: "GRL_TRY_HANDLER_MISSING" })
);
});
});

View File

@@ -0,0 +1,149 @@
import { describe, expect, it } from "vitest";
import { generateGrlProgram, type GrlProgramGenerationSpec } from "../../../src/grl/generator/index.js";
import { postProcessAllBrands } from "../../../src/grl/post/index.js";
import type { GrlOperationDeclaration, GrlPathDeclaration, GrlTargetDeclaration } from "../../../src/grl/ast/index.js";
import { parseGrl } from "../../../src/grl/parser/index.js";
import { compileSemanticProgram } from "../../../src/grl/semantic/index.js";
const SPEC: GrlProgramGenerationSpec = {
moduleName: "GeneratedCell",
speeds: {
v_linear: "linear(200 mm/s)",
v_joint: "joint(50 %)"
},
zones: {
z10: "z(10 mm)",
zf: "fine"
},
targets: [
{ name: "pick", kind: "pose", values: [500, 0, 0, 0, 0, 0] },
{ name: "home", kind: "joint", values: [0] },
{ name: "place", kind: "pose", values: [600, 0, 0, 0, 0, 0] }
],
path: {
name: "generated_path",
source: {
type: "cad_curve",
id: "edge_001",
sample_distance: 5
},
defaults: {
speed: "v_linear",
zone: "z10"
},
points: [
{ motion: "movej", target: "home", speed: "v_joint", zone: "zf" },
{ motion: "movel", target: "pick" },
{ id: "place_point", motion: "movel", target: "place", zone: "zf" }
]
},
operation: {
name: "generated_op",
kind: "handling",
path: "generated_path",
startAction: "io.do[1] = true",
endAction: "io.do[1] = false"
}
};
describe("GRL generator and roundtrip", () => {
it("generates stable expanded GRL with target/path/operation first", () => {
const first = generateGrlProgram(SPEC, "expanded");
const second = generateGrlProgram(SPEC, "expanded");
expect(first).toEqual(second);
expect(first.stableIds).toEqual({
targets: ["home", "pick", "place"],
points: ["p00", "p01", "place_point"],
path: "generated_path",
operation: "generated_op"
});
expect(first.text).toBe(`language grl 0.1
module GeneratedCell
const speed v_joint = joint(50 %)
const speed v_linear = linear(200 mm/s)
const zone z10 = z(10 mm)
const zone zf = fine
target home = joint_target {
joints: [0 deg]
}
target pick = pose_target {
pose: pose(500 mm, 0 mm, 0 mm, 0 deg, 0 deg, 0 deg)
}
target place = pose_target {
pose: pose(600 mm, 0 mm, 0 mm, 0 deg, 0 deg, 0 deg)
}
path generated_path {
source {
id: "edge_001"
sample_distance: 5
type: cad_curve
}
defaults {
speed: v_linear
zone: z10
}
point p00 movej home speed v_joint zone zf
point p01 movel pick
point place_point movel place zone zf
}
operation generated_op {
kind: handling
path: generated_path
start_action:
io.do[1] = true
end_action:
io.do[1] = false
}
proc main()
run_operation generated_op
end
end`);
});
it("supports compact output that remains parseable", () => {
const compact = generateGrlProgram(SPEC, "compact");
expect(compact.text).toContain("path generated_path { source { id: \"edge_001\"");
expect(parseGrl(compact.text).module.name).toBe("GeneratedCell");
});
it("roundtrips through parser, semantic IR, and postprocessors", () => {
const generated = generateGrlProgram(SPEC, "expanded");
const ast = parseGrl(generated.text);
const declarations = ast.module.declarations;
const targets = declarations.filter((decl): decl is GrlTargetDeclaration => decl.kind === "TargetDeclaration");
const path = declarations.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!;
const operation = declarations.find((decl): decl is GrlOperationDeclaration => decl.kind === "OperationDeclaration")!;
const ir = compileSemanticProgram(ast, {
startJoints: [0],
sampleTime: 0.004
});
const post = postProcessAllBrands(ir);
expect(targets.map((target) => target.name)).toEqual(["home", "pick", "place"]);
expect(path.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "PathSourceBlock" }),
expect.objectContaining({ kind: "PathDefaultsBlock" }),
expect.objectContaining({ kind: "PathPoint", id: "p00" }),
expect.objectContaining({ kind: "PathPoint", id: "p01" }),
expect.objectContaining({ kind: "PathPoint", id: "place_point" })
])
);
expect(operation).toMatchObject({
name: "generated_op",
operationKind: "handling",
pathName: "generated_path"
});
expect(ir.symbols).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "path", name: "generated_path" }),
expect.objectContaining({ kind: "operation", name: "generated_op" })
])
);
expect(post.outputs.abb.text).toContain("MODULE GeneratedCell");
expect(post.outputs.fanuc.text).toContain("/PROG MAIN");
expect(post.outputs.kuka.text).toContain("DEF Main()");
});
});

View File

@@ -0,0 +1,205 @@
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
})
]);
});
});

View File

@@ -0,0 +1,94 @@
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");
});
});

View File

@@ -0,0 +1,124 @@
import { describe, expect, it } from "vitest";
import type { GrlDataDeclaration, GrlProcedureDeclaration, GrlTargetDeclaration } from "../../src/grl/ast/index.js";
import { parseGrl } from "../../src/grl/parser/index.js";
import {
buildMotionContext,
compileMotionToKdlRequest,
parseProcedureMotionInstructions
} 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),
mass: 1 kg
}
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),
tool: gripper,
frame: fixture
}
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)
}
proc main()
set_tool gripper
set_frame fixture
set_speed v_linear
set_zone z10
movej home speed v_joint zone fine
movel pick
movec via mid target arc_end speed linear(150 mm/s) zone fine
end
end
`;
function declarations() {
return parseGrl(PROGRAM).module.declarations;
}
describe("GRL motion instruction compilation", () => {
it("parses movej, movel, and movec from procedure body tokens", () => {
const decls = declarations();
const context = buildMotionContext(
decls.filter(
(decl): decl is GrlDataDeclaration | GrlTargetDeclaration =>
decl.kind === "DataDeclaration" || decl.kind === "TargetDeclaration"
)
);
const procedure = decls.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!;
const instructions = parseProcedureMotionInstructions(procedure, context);
expect(instructions.map((instruction) => instruction.kind)).toEqual(["MOVEJ", "MOVEL", "MOVEC"]);
expect(instructions[0]).toMatchObject({
kind: "MOVEJ",
speed: { kind: "joint_percent", value: 0.6 },
zone: { kind: "fine" },
target: { joints: [0, 0] },
sourceMap: { line: 32 }
});
expect(instructions[1]).toMatchObject({
kind: "MOVEL",
speed: { kind: "linear", velocity: 0.3 },
zone: { kind: "distance", value: 0.01 }
});
expect(instructions[1]?.tool?.position).toEqual([0, 0, 0.1]);
expect(instructions[1]?.frame?.position).toEqual([0.8, 0, 0]);
expect(instructions[2]).toMatchObject({
kind: "MOVEC",
speed: { kind: "linear", velocity: 0.15 },
zone: { kind: "fine" }
});
});
it("compiles motion instructions to KDL request shapes", () => {
const decls = declarations();
const context = buildMotionContext(
decls.filter(
(decl): decl is GrlDataDeclaration | GrlTargetDeclaration =>
decl.kind === "DataDeclaration" || decl.kind === "TargetDeclaration"
)
);
const procedure = decls.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!;
const [movej, movel, movec] = parseProcedureMotionInstructions(procedure, context);
expect(compileMotionToKdlRequest(movej!, { startJoints: [0, 0], sampleTime: 0.004 })).toMatchObject({
startJoints: [0, 0],
target: { joints: [0, 0] },
speed: { kind: "joint_percent", value: 0.6 },
zone: { kind: "fine" },
sampleTime: 0.004
});
expect(compileMotionToKdlRequest(movel!, { startJoints: [0, 0], sampleTime: 0.004 })).toMatchObject({
startJoints: [0, 0],
target: { pose: { position: [0.5, 0, 0] } },
speed: { kind: "linear", velocity: 0.3 },
zone: { kind: "distance", value: 0.01 },
tool: { position: [0, 0, 0.1] },
frame: { position: [0.8, 0, 0] },
sampleTime: 0.004
});
expect(compileMotionToKdlRequest(movec!, { startJoints: [0, 0], sampleTime: 0.004 })).toMatchObject({
startJoints: [0, 0],
via: { pose: { position: [0.55, 0.05, 0] } },
target: { pose: { position: [0.6, 0, 0] } },
speed: { kind: "linear", velocity: 0.15 },
zone: { kind: "fine" },
sampleTime: 0.004
});
});
});

View File

@@ -0,0 +1,153 @@
import { describe, expect, it } from "vitest";
import type {
GrlOperationDeclaration,
GrlPathDeclaration,
GrlProcedureDeclaration
} from "../../src/grl/ast/index.js";
import { parseGrl } from "../../src/grl/parser/index.js";
import {
compileOperation,
expandRunOperation,
parseProcedureRunOperationStatements
} 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 weld_path {
defaults { speed: v, zone: zf }
point p0 movej home
}
operation weld_op_01 {
kind: arc_welding
path: weld_path
process {
weld_id: "WELD_1"
voltage: 24.0
current: 180.0
weave: none
}
start_action:
io.do[20] = true
end_action:
io.do[20] = false
}
proc main()
run_operation weld_op_01
end
end
`;
function declarations() {
return parseGrl(PROGRAM).module.declarations;
}
function pathsByName(paths: GrlPathDeclaration[]) {
return new Map(paths.map((path) => [path.name, path]));
}
describe("GRL operation compilation", () => {
it("parses operation kind, path, process, and action blocks", () => {
const operation = declarations().find(
(decl): decl is GrlOperationDeclaration => decl.kind === "OperationDeclaration"
)!;
expect(operation).toMatchObject({
kind: "OperationDeclaration",
name: "weld_op_01",
operationKind: "arc_welding",
pathName: "weld_path",
items: [
{ kind: "OperationProcessBlock" },
{ kind: "OperationActionBlock", actionKind: "start_action" },
{ kind: "OperationActionBlock", actionKind: "end_action" }
]
});
});
it("compiles operation process metadata and action statements", () => {
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(compiled).toMatchObject({
operationId: "weld_op_01",
kind: "arc_welding",
pathId: "weld_path",
process: {
weld_id: "WELD_1",
voltage: 24,
current: 180,
weave: "none"
},
startActions: [
{
kind: "ACTION",
actionKind: "start_action",
operationId: "weld_op_01",
statement: "io . do [ 20 ] = true"
}
],
endActions: [
{
kind: "ACTION",
actionKind: "end_action",
operationId: "weld_op_01",
statement: "io . do [ 20 ] = false"
}
]
});
});
it("extracts run_operation and expands to start action, path, and end action", () => {
const decls = declarations();
const procedure = decls.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!;
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));
const [run] = parseProcedureRunOperationStatements(procedure);
expect(run).toEqual({
kind: "RUN_OPERATION",
operationId: "weld_op_01",
sourceMap: {
line: 25,
column: 5
}
});
expect(expandRunOperation(run!, new Map([[compiled.operationId, compiled]]))).toEqual([
expect.objectContaining({ kind: "ACTION", actionKind: "start_action" }),
{
kind: "RUN_PATH",
pathId: "weld_path",
sourceMap: {
line: 25,
column: 5
}
},
expect.objectContaining({ kind: "ACTION", actionKind: "end_action" })
]);
});
it("reports operations that reference missing paths and missing run_operation targets", () => {
const missingPathOperation = parseGrl(`language grl 0.1
module Main
operation bad_op {
kind: handling
path: missing_path
}
end
`).module.declarations.find((decl): decl is GrlOperationDeclaration => decl.kind === "OperationDeclaration")!;
expect(() => compileOperation(missingPathOperation, new Map())).toThrowError(
expect.objectContaining({ code: "GRL_OPERATION_PATH_NOT_FOUND" })
);
expect(() =>
expandRunOperation({ kind: "RUN_OPERATION", operationId: "missing_op" }, new Map())
).toThrowError(expect.objectContaining({ code: "GRL_OPERATION_NOT_FOUND" }));
});
});

View File

@@ -0,0 +1,103 @@
import { describe, expect, it } from "vitest";
import { GrlParseError, parseGrl } from "../../src/grl/parser/index.js";
describe("GRL parser", () => {
it("parses the minimal language/module/proc skeleton with source ranges", () => {
const ast = parseGrl(`language grl 0.1
module Main
proc main()
// body comment should not affect parser
end
end
`);
expect(ast).toMatchObject({
kind: "Program",
language: {
kind: "LanguageDeclaration",
language: "grl",
version: "0.1",
range: {
start: { line: 1, column: 1 },
end: { line: 1, column: 17 }
}
},
module: {
kind: "ModuleDeclaration",
name: "Main",
declarations: [
{
kind: "ProcedureDeclaration",
name: "main",
params: []
}
]
}
});
expect(ast.module.range.start).toMatchObject({ line: 3, column: 1 });
expect(ast.module.range.end).toMatchObject({ line: 7, column: 4 });
});
it("parses imports, data declarations, targets, and procedure body tokens", () => {
const ast = parseGrl(`language grl 0.1
module Main
import CommonTools
const speed v_pick = linear(300 mm/s)
target home = joint_target {
joints: [0 deg, 0 deg]
}
proc main()
movej home
end
end
`);
expect(ast.module.declarations.map((decl) => decl.kind)).toEqual([
"ImportDeclaration",
"DataDeclaration",
"TargetDeclaration",
"ProcedureDeclaration"
]);
expect(ast.module.declarations[0]).toMatchObject({
kind: "ImportDeclaration",
moduleName: "CommonTools"
});
expect(ast.module.declarations[1]).toMatchObject({
kind: "DataDeclaration",
storage: "const",
typeName: "speed",
name: "v_pick",
initializer: {
kind: "CallExpression",
callee: "linear"
}
});
expect(ast.module.declarations[2]).toMatchObject({
kind: "TargetDeclaration",
name: "home",
target: {
kind: "ObjectExpression",
typeName: "joint_target"
}
});
expect(ast.module.declarations[3]).toMatchObject({
kind: "ProcedureDeclaration",
bodyTokens: [
{
kind: "keyword",
raw: "movej"
},
{
kind: "identifier",
raw: "home"
}
]
});
});
it("reports stable line and column on invalid syntax", () => {
expect(() => parseGrl("language grl\nmodule Main\nend\n")).toThrow(GrlParseError);
expect(() => parseGrl("language grl\nmodule Main\nend\n")).toThrow("Expected GRL language version at 2:1");
});
});

View File

@@ -0,0 +1,251 @@
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" }));
});
});

View File

@@ -0,0 +1,201 @@
import { describe, expect, it } from "vitest";
import type { GrlFunctionDeclaration, GrlProcedureDeclaration } from "../../src/grl/ast/index.js";
import { parseGrl } from "../../src/grl/parser/index.js";
import { analyzeProcFunctionSemantics } from "../../src/grl/semantic/index.js";
function declarations(source: string) {
return parseGrl(source).module.declarations;
}
describe("GRL proc, func, call, return, and scope semantics", () => {
it("parses function declarations and analyzes proc/func signatures, calls, returns, and warnings", () => {
const decls = declarations(`language grl 0.1
module Main
var int global_count = 0
proc read_sensor(out bool ok)
ok = true
end
proc main(in bool start, out bool done, inout int count)
call read_sensor(done)
call helper(count)
call self_check()
return
end
proc self_check()
call self_check()
end
func int helper(inout int value)
var int global_count = 1
return value
end
end
`);
const func = decls.find((decl): decl is GrlFunctionDeclaration => decl.kind === "FunctionDeclaration")!;
expect(func).toMatchObject({
kind: "FunctionDeclaration",
returnType: "int",
name: "helper",
bodyTokens: [
{ raw: "var" },
{ raw: "int" },
{ raw: "global_count" },
{ raw: "=" },
{ raw: "1" },
{ raw: "return" },
{ raw: "value" }
]
});
const analysis = analyzeProcFunctionSemantics(decls);
expect(analysis.procedures).toEqual([
expect.objectContaining({
name: "read_sensor",
parameters: [expect.objectContaining({ name: "ok", typeName: "bool", direction: "out" })]
}),
expect.objectContaining({
name: "main",
parameters: [
expect.objectContaining({ name: "start", typeName: "bool", direction: "in" }),
expect.objectContaining({ name: "done", typeName: "bool", direction: "out" }),
expect.objectContaining({ name: "count", typeName: "int", direction: "inout" })
]
}),
expect.objectContaining({ name: "self_check", parameters: [] })
]);
expect(analysis.functions).toEqual([
expect.objectContaining({
name: "helper",
returnType: "int",
parameters: [expect.objectContaining({ name: "value", typeName: "int", direction: "inout" })]
})
]);
expect(analysis.calls).toEqual([
expect.objectContaining({ kind: "CALL", target: "read_sensor", args: [expect.objectContaining({ text: "done" })] }),
expect.objectContaining({ kind: "CALL", target: "helper", args: [expect.objectContaining({ text: "count" })] }),
expect.objectContaining({ kind: "CALL", target: "self_check", args: [] }),
expect.objectContaining({ kind: "CALL", target: "self_check", args: [] })
]);
expect(analysis.returns).toEqual([
expect.objectContaining({ kind: "RETURN" }),
expect.objectContaining({ kind: "RETURN", value: expect.objectContaining({ text: "value" }) })
]);
expect(analysis.diagnostics).toEqual([
expect.objectContaining({ severity: "warning", code: "GRL_RECURSIVE_CALL" }),
expect.objectContaining({ severity: "warning", code: "GRL_NAME_SHADOWS_OUTER_SCOPE" })
]);
});
it("reports out parameters that are not assigned on all normal return paths", () => {
const decls = declarations(`language grl 0.1
module Main
proc main(out bool done)
if ready == true
done = true
end
return
end
end
`);
expect(() => analyzeProcFunctionSemantics(decls)).toThrowError(
expect.objectContaining({ code: "GRL_OUT_PARAM_NOT_ASSIGNED" })
);
});
it("reports out and inout call arguments that are not lvalues", () => {
const decls = declarations(`language grl 0.1
module Main
proc set_done(out bool done)
done = true
end
proc main()
call set_done(true)
end
end
`);
expect(() => analyzeProcFunctionSemantics(decls)).toThrowError(
expect.objectContaining({ code: "GRL_ARGUMENT_NOT_LVALUE" })
);
});
it("reports missing or incompatible function returns", () => {
const missingReturn = declarations(`language grl 0.1
module Main
func int bad(in bool ready)
if ready == true
return 1
end
end
end
`);
const wrongReturn = declarations(`language grl 0.1
module Main
func bool bad()
return 1
end
end
`);
expect(() => analyzeProcFunctionSemantics(missingReturn)).toThrowError(
expect.objectContaining({ code: "GRL_FUNC_MISSING_RETURN" })
);
expect(() => analyzeProcFunctionSemantics(wrongReturn)).toThrowError(
expect.objectContaining({ code: "GRL_RETURN_TYPE_MISMATCH" })
);
});
it("reports illegal function side effects and procedure return values", () => {
const functionSideEffect = declarations(`language grl 0.1
module Main
func bool bad()
wait io.di[1] == true
return true
end
end
`);
const procedureReturnValue = declarations(`language grl 0.1
module Main
proc main()
return true
end
end
`);
expect(() => analyzeProcFunctionSemantics(functionSideEffect)).toThrowError(
expect.objectContaining({ code: "GRL_FUNC_SIDE_EFFECT" })
);
expect(() => analyzeProcFunctionSemantics(procedureReturnValue)).toThrowError(
expect.objectContaining({ code: "GRL_RETURN_VALUE_IN_PROC" })
);
});
it("reports call target and argument type errors", () => {
const missingCall = declarations(`language grl 0.1
module Main
proc main()
call missing()
end
end
`);
const typeMismatch = declarations(`language grl 0.1
module Main
proc expects_int(in int value)
return
end
proc main()
call expects_int("bad")
end
end
`);
expect(() => analyzeProcFunctionSemantics(missingCall)).toThrowError(
expect.objectContaining({ code: "GRL_CALL_TARGET_NOT_FOUND" })
);
expect(() => analyzeProcFunctionSemantics(typeMismatch)).toThrowError(
expect.objectContaining({ code: "GRL_CALL_ARGUMENT_TYPE" })
);
});
});

View File

@@ -0,0 +1,148 @@
import { describe, expect, it } from "vitest";
import { parseGrl } from "../../src/grl/parser/index.js";
import { compileSemanticProgram } from "../../src/grl/semantic/index.js";
const PROGRAM = `language grl 0.1
module Main
const speed vj = joint(50 %)
const speed vl = linear(200 mm/s)
const zone zf = fine
target home = joint_target { joints: [0 deg] }
target pick = pose_target { pose: pose(500 mm, 0 mm, 0 mm, 0 deg, 0 deg, 0 deg) }
path pick_path {
defaults { speed: vl, zone: zf }
point p0 movej home speed vj zone fine
point p1 movel pick
event before p1 io.do[1] = true
}
operation pick_op {
kind: handling
path: pick_path
start_action:
io.do[2] = true
end_action:
io.do[2] = false
}
proc set_done(out bool done)
done = true
end
proc main(out bool done)
set_speed vl
set_zone zf
movej home speed vj zone fine
io.do[3] = true
wait io.di[1] == true timeout 1 s
if done == false
pulse io.do[4] duration 100 ms
else
alarm DONE "Already done"
end
call set_done(done)
run_path pick_path
run_operation pick_op
return
end
end
`;
describe("GRL semantic analyzer, executable IR, and source map", () => {
it("compiles a complete program into unified executable IR and KDL bridge requests", () => {
const ir = compileSemanticProgram(parseGrl(PROGRAM), {
startJoints: [0],
sampleTime: 0.004
});
expect(ir.moduleName).toBe("Main");
expect(ir.semanticChecks).toHaveLength(22);
expect(ir.symbols).toEqual([
expect.objectContaining({ kind: "data", name: "vj", typeName: "speed" }),
expect.objectContaining({ kind: "data", name: "vl", typeName: "speed" }),
expect.objectContaining({ kind: "data", name: "zf", typeName: "zone" }),
expect.objectContaining({ kind: "target", name: "home" }),
expect.objectContaining({ kind: "target", name: "pick" }),
expect.objectContaining({ kind: "path", name: "pick_path" }),
expect.objectContaining({ kind: "operation", name: "pick_op" }),
expect.objectContaining({ kind: "procedure", name: "set_done" }),
expect.objectContaining({ kind: "procedure", name: "main" })
]);
expect(ir.paths).toHaveLength(1);
expect(ir.operations).toHaveLength(1);
expect(ir.kdlBridge.pathRequests).toEqual([
expect.objectContaining({
pathId: "pick_path",
segments: [
expect.objectContaining({ id: "p0", motion: "MOVEJ" }),
expect.objectContaining({ id: "p1", motion: "MOVEL" })
]
})
]);
expect(ir.kdlBridge.motionRequests).toEqual([
expect.objectContaining({
startJoints: [0],
speed: { kind: "joint_percent", value: 0.5 },
zone: { kind: "fine" },
sampleTime: 0.004
})
]);
const main = ir.procedures.find((procedure) => procedure.name === "main")!;
expect(main.instructions).toEqual([
expect.objectContaining({ kind: "MOVEJ" }),
expect.objectContaining({ kind: "IO_WRITE", target: expect.objectContaining({ domain: "do", index: 3 }) }),
expect.objectContaining({ kind: "WAIT", timeout: 1 }),
expect.objectContaining({
kind: "EXEC_IF",
branches: [
expect.objectContaining({
branchKind: "if",
body: expect.arrayContaining([expect.objectContaining({ kind: "PULSE", duration: 0.1 })])
}),
expect.objectContaining({
branchKind: "else",
body: expect.arrayContaining([expect.objectContaining({ kind: "ALARM", alarmId: "DONE" })])
})
]
}),
expect.objectContaining({ kind: "CALL", target: "set_done" }),
expect.objectContaining({ kind: "RUN_PATH", pathId: "pick_path" }),
expect.objectContaining({ kind: "RUN_OPERATION", operationId: "pick_op" }),
expect.objectContaining({ kind: "RETURN" })
]);
});
it("exposes source map entries for GRL procedure lines, path points, and operation actions", () => {
const ir = compileSemanticProgram(parseGrl(PROGRAM), {
startJoints: [0],
sampleTime: 0.004
});
expect(ir.sourceMap).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "path_point", pathId: "pick_path", pointId: "p0" }),
expect.objectContaining({ kind: "path_point", pathId: "pick_path", pointId: "p1" }),
expect.objectContaining({ kind: "operation_action", operationId: "pick_op" }),
expect.objectContaining({ kind: "MOVEJ", procedureId: "main", sourceMap: expect.objectContaining({ line: 28 }) }),
expect.objectContaining({ kind: "EXEC_IF", procedureId: "main" }),
expect.objectContaining({ kind: "RUN_OPERATION", procedureId: "main" })
])
);
});
it("reports duplicate symbols through semantic diagnostics", () => {
const ir = compileSemanticProgram(parseGrl(`language grl 0.1
module Main
const speed v = joint(10 %)
const speed v = joint(20 %)
proc main()
end
end
`), {
startJoints: [],
sampleTime: 0.004
});
expect(ir.diagnostics).toEqual([
expect.objectContaining({ severity: "error", code: "GRL_SYMBOL_DUPLICATE" })
]);
});
});