142 lines
5.5 KiB
TypeScript
142 lines
5.5 KiB
TypeScript
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 })
|
|
]);
|
|
});
|
|
});
|