Files
KDL_WORK/kdl-wasm/web/src/grl/semantic/compileIo.ts
2026-06-28 08:20:33 +08:00

471 lines
14 KiB
TypeScript

import { KdlStructuredError } from "../../kdl/rpc.js";
import type { MotionSourceMap } from "../../kdl/types.js";
import type {
IoFlowInstruction,
IoReference,
IoWriteInstruction,
OperationActionInstruction,
PathEventInstruction,
PulseInstruction,
WaitInstruction
} from "../ir/index.js";
import { lexGrl, type GrlToken } from "../lexer/index.js";
import { parseGrlExpression } from "../parser/index.js";
import { evaluateNumberExpression } from "./constantExpression.js";
import type { GrlNumberLiteral } from "../ast/index.js";
export interface IoMap {
aliases?: Record<string, IoReference>;
allowedRanges?: Partial<Record<IoReference["domain"], { min: number; max: number }>>;
}
export function parseIoFlowStatements(tokens: GrlToken[], ioMap: IoMap = {}): IoFlowInstruction[] {
const parser = new IoStatementParser(tokens, ioMap);
return parser.parseAll();
}
export function compilePathEventIo(event: PathEventInstruction, ioMap: IoMap = {}): IoFlowInstruction[] {
const tokens = event.data?.tokens;
if (Array.isArray(tokens)) {
return parseIoFlowStatements(tokens as GrlToken[], ioMap);
}
const statement = event.data?.statement;
if (typeof statement !== "string") {
return [];
}
return compileStatementString(statement, event.sourceMap, ioMap);
}
export function compileOperationActionIo(
action: OperationActionInstruction,
ioMap: IoMap = {}
): IoFlowInstruction[] {
if (Array.isArray(action.tokens)) {
return parseIoFlowStatements(action.tokens as GrlToken[], ioMap);
}
return compileStatementString(action.statement, action.sourceMap, ioMap);
}
class IoStatementParser {
private current = 0;
constructor(
private readonly tokens: GrlToken[],
private readonly ioMap: IoMap
) {}
parseAll(): IoFlowInstruction[] {
const instructions: IoFlowInstruction[] = [];
while (!this.isAtEnd()) {
if (this.checkKeyword("wait")) {
instructions.push(this.finishWait(this.advance()));
continue;
}
if (this.checkKeyword("pulse")) {
instructions.push(this.finishPulse(this.advance()));
continue;
}
if (this.checkIoStart()) {
instructions.push(this.finishIoWrite(this.peek()));
continue;
}
this.advance();
}
return instructions;
}
private finishIoWrite(start: GrlToken): IoWriteInstruction {
const target = this.parseIoReference();
this.consumeOperator("=", "Expected = in IO assignment");
const value = this.parseValue();
return {
kind: "IO_WRITE",
target,
value,
sourceMap: tokenSourceMap(start)
};
}
private finishWait(start: GrlToken): WaitInstruction {
const conditionTokens = this.collectUntilKeyword(["timeout", "on_timeout"]);
if (conditionTokens.length === 0) {
throw ioError("GRL_WAIT_CONDITION_MISSING", "wait requires a condition");
}
let timeout: number | undefined;
let onTimeout: WaitInstruction["onTimeout"];
if (this.matchKeyword("timeout")) {
timeout = this.parseDuration();
}
if (this.matchKeyword("on_timeout")) {
const kind = this.consumeIdentifier("Expected on_timeout action").raw;
if (kind === "alarm") {
const message = this.consumeString("Expected alarm message");
onTimeout = { kind: "alarm", value: message.value };
} else if (kind === "call") {
onTimeout = { kind: "call", value: this.collectRest().map((token) => token.raw).join(" ") };
} else {
throw ioError("GRL_WAIT_TIMEOUT_ACTION_INVALID", `Unsupported on_timeout action ${kind}`);
}
}
return {
kind: "WAIT",
condition: conditionTokens.map((token) => token.raw).join(" "),
...(timeout !== undefined ? { timeout } : {}),
...(onTimeout ? { onTimeout } : {}),
sourceMap: tokenSourceMap(start)
};
}
private finishPulse(start: GrlToken): PulseInstruction {
const target = this.parseIoReference();
this.consumeKeyword("duration", "Expected duration in pulse");
const duration = this.parseDuration();
return {
kind: "PULSE",
target,
duration,
trace: [
{ time: 0, action: "set", target, value: true },
{ time: duration, action: "reset", target, value: false }
],
sourceMap: tokenSourceMap(start)
};
}
private parseIoReference(): IoReference {
const io = this.consumeKeyword("io", "Expected io reference");
this.consumePunctuation(".", "Expected . after io");
const domain = this.consumeIdentifier("Expected IO domain").raw as IoReference["domain"];
if (!["di", "do", "ai", "ao", "gi", "go", "ri", "ro", "alias"].includes(domain)) {
throw ioError("GRL_IO_DOMAIN_INVALID", `Unsupported IO domain ${domain}`);
}
if (domain === "alias") {
this.consumePunctuation(".", "Expected . after io.alias");
const alias = this.consumeIdentifier("Expected IO alias").raw;
const mapped = this.ioMap.aliases?.[alias];
return mapped ?? { domain, alias, raw: `io.alias.${alias}` };
}
this.consumePunctuation("[", "Expected [ after IO domain");
const indexToken = this.consumeNumber("Expected IO index");
this.consumePunctuation("]", "Expected ] after IO index");
const index = indexToken.value;
this.validateIoIndex(domain, index, io);
return {
domain,
index,
raw: `io.${domain}[${index}]`
};
}
private validateIoIndex(domain: IoReference["domain"], index: number, token: GrlToken): void {
if (!Number.isInteger(index) || index < 0) {
throw ioError("GRL_IO_INDEX_INVALID", `Invalid IO index ${index}`);
}
const range = this.ioMap.allowedRanges?.[domain];
if (range && (index < range.min || index > range.max)) {
throw new KdlStructuredError(
"GRL_IO_ADDRESS_NOT_FOUND",
`IO address io.${domain}[${index}] is outside [${range.min}, ${range.max}]`,
[
{
severity: "error",
code: "GRL_IO_ADDRESS_NOT_FOUND",
message: `IO address io.${domain}[${index}] is outside [${range.min}, ${range.max}]`,
sourceMap: tokenSourceMap(token)
}
]
);
}
}
private parseValue(): boolean | number | string {
const token = this.advance();
if (token.kind === "keyword" && (token.raw === "true" || token.raw === "false")) {
return token.raw === "true";
}
if (token.kind === "number") {
return evaluateNumberExpression(numberLiteralFromToken(token));
}
if (token.kind === "string") {
return token.value;
}
if (token.kind === "identifier" || token.kind === "keyword") {
return token.raw;
}
throw ioError("GRL_IO_VALUE_INVALID", "Unsupported IO assignment value");
}
private parseDuration(): number {
const tokens = this.collectDurationExpressionTokens();
return evaluateNumberExpression(parseGrlExpression(tokens), { expectedKind: "time", defaultUnit: "s" });
}
private collectDurationExpressionTokens(): GrlToken[] {
const tokens: GrlToken[] = [];
let parenDepth = 0;
let bracketDepth = 0;
while (!this.isAtEnd()) {
const token = this.peek();
if (
tokens.length > 0 &&
parenDepth === 0 &&
bracketDepth === 0 &&
(this.isKeywordLike(token, "on_timeout") || this.isCurrentStatementStartAfter(tokens))
) {
break;
}
const consumed = this.advance();
tokens.push(consumed);
if (consumed.kind === "punctuation" && consumed.raw === "(") {
parenDepth += 1;
} else if (consumed.kind === "punctuation" && consumed.raw === ")") {
parenDepth = Math.max(0, parenDepth - 1);
} else if (consumed.kind === "punctuation" && consumed.raw === "[") {
bracketDepth += 1;
} else if (consumed.kind === "punctuation" && consumed.raw === "]") {
bracketDepth = Math.max(0, bracketDepth - 1);
}
}
if (tokens.length === 0) {
throw ioError("GRL_TOKEN_EXPECTED", "Expected duration");
}
return tokens;
}
private collectUntilKeyword(keywords: string[]): GrlToken[] {
const tokens: GrlToken[] = [];
let parenDepth = 0;
while (!this.isAtEnd()) {
const token = this.peek();
if (parenDepth === 0) {
if ((token.kind === "keyword" || token.kind === "identifier") && keywords.includes(token.raw)) {
break;
}
if (tokens.length > 0 && this.isCurrentStatementStartAfter(tokens)) {
break;
}
}
const consumed = this.advance();
tokens.push(consumed);
if (consumed.kind === "punctuation" && consumed.raw === "(") {
parenDepth += 1;
} else if (consumed.kind === "punctuation" && consumed.raw === ")") {
parenDepth = Math.max(0, parenDepth - 1);
}
}
return tokens;
}
private collectRest(): GrlToken[] {
const tokens: GrlToken[] = [];
while (!this.isAtEnd()) {
if (tokens.length > 0 && this.isCurrentStatementStartAfter(tokens)) {
break;
}
tokens.push(this.advance());
}
return tokens;
}
private checkIoStart(): boolean {
return this.isIoStartAtCurrent();
}
private isCurrentStatementStartAfter(tokens: GrlToken[]): boolean {
const token = this.peek();
const previous = tokens.at(-1);
if (!previous || token.range.start.line <= previous.range.end.line) {
return false;
}
if (this.isStatementStart(token)) {
return true;
}
if (!this.isIoStartAtCurrent()) {
return false;
}
return true;
}
private isIoStartAtCurrent(): boolean {
return this.peek().raw === "io" && this.maybePeek(1)?.raw === ".";
}
private isStatementStart(token: GrlToken): boolean {
return (
(token.kind === "keyword" || token.kind === "identifier") &&
[
"io",
"wait",
"pulse",
"movej",
"movel",
"movec",
"set_tool",
"set_frame",
"set_speed",
"set_zone",
"run_path",
"run_operation",
"if",
"elseif",
"else",
"while",
"for",
"switch",
"case",
"default",
"break",
"continue",
"label",
"jump",
"call",
"return",
"alarm",
"raise",
"try",
"catch",
"finally",
"end"
].includes(token.raw)
);
}
private isKeywordLike(token: GrlToken, keyword: string): boolean {
return (token.kind === "keyword" || token.kind === "identifier") && token.raw === keyword;
}
private matchKeyword(keyword: string): boolean {
const token = this.peek();
if ((token.kind === "keyword" || token.kind === "identifier") && token.raw === keyword) {
this.advance();
return true;
}
return false;
}
private checkKeyword(keyword: string): boolean {
const token = this.peek();
return (token.kind === "keyword" || token.kind === "identifier") && token.raw === keyword;
}
private consumeKeyword(keyword: string, message: string): GrlToken {
if (this.checkKeyword(keyword)) {
return this.advance();
}
throw ioError("GRL_KEYWORD_EXPECTED", message);
}
private consumeIdentifier(message: string): GrlToken {
const token = this.peek();
if (token.kind === "identifier" || token.kind === "keyword") {
return this.advance();
}
throw ioError("GRL_IDENTIFIER_EXPECTED", message);
}
private consumePunctuation(value: string, message: string): GrlToken {
const token = this.peek();
if (token.kind === "punctuation" && token.raw === value) {
return this.advance();
}
throw ioError("GRL_PUNCTUATION_EXPECTED", message);
}
private consumeOperator(value: string, message: string): GrlToken {
const token = this.peek();
if (token.kind === "operator" && token.raw === value) {
return this.advance();
}
throw ioError("GRL_OPERATOR_EXPECTED", message);
}
private consume(kind: GrlToken["kind"], message: string): GrlToken {
if (this.peek().kind === kind) {
return this.advance();
}
throw ioError("GRL_TOKEN_EXPECTED", message);
}
private consumeNumber(message: string): Extract<GrlToken, { kind: "number" }> {
const token = this.peek();
if (token.kind === "number") {
return this.advance() as Extract<GrlToken, { kind: "number" }>;
}
throw ioError("GRL_TOKEN_EXPECTED", message);
}
private consumeString(message: string): Extract<GrlToken, { kind: "string" }> {
const token = this.peek();
if (token.kind === "string") {
return this.advance() as Extract<GrlToken, { kind: "string" }>;
}
throw ioError("GRL_TOKEN_EXPECTED", message);
}
private advance(): GrlToken {
this.current += 1;
return this.previous();
}
private previous(): GrlToken {
return this.tokens[this.current - 1]!;
}
private peek(): GrlToken {
return this.tokens[this.current]!;
}
private maybePeek(distance = 0): GrlToken | undefined {
return this.tokens[this.current + distance];
}
private isAtEnd(): boolean {
return this.current >= this.tokens.length;
}
}
function compileStatementString(
statement: string,
_sourceMap: MotionSourceMap | undefined,
ioMap: IoMap
): IoFlowInstruction[] {
const tokens = lexGrl(statement, { preserveComments: false }).filter(
(token) => token.kind !== "eof" && token.kind !== "comment"
);
return parseIoFlowStatements(tokens, ioMap);
}
function tokenSourceMap(token: GrlToken): MotionSourceMap {
return {
line: token.range.start.line,
column: token.range.start.column
};
}
function numberLiteralFromToken(token: Extract<GrlToken, { kind: "number" }>): GrlNumberLiteral {
return {
kind: "NumberLiteral",
value: token.value,
raw: token.raw,
...(token.unit
? {
unit: {
raw: token.unit.raw,
kind: token.unit.kind,
siUnit: token.unit.siUnit,
normalizedValue: token.unit.normalizedValue
}
}
: {}),
range: token.range
};
}
function ioError(code: string, message: string): KdlStructuredError {
return new KdlStructuredError(code, message);
}