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; allowedRanges?: Partial>; } 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 { if (this.isAtEnd()) { return false; } 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 { if (this.isAtEnd()) { return false; } 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 { const token = this.peek(); if (token.kind === "number") { return this.advance() as Extract; } throw ioError("GRL_TOKEN_EXPECTED", message); } private consumeString(message: string): Extract { const token = this.peek(); if (token.kind === "string") { return this.advance() as Extract; } 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): 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); }