Files
KDL_WORK/kdl-wasm/web/tests/grl/expressionCompile.test.ts
2026-06-28 08:20:33 +08:00

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 })
]);
});
});