同步KDL工程源码到云仓库
This commit is contained in:
380
kdl-wasm/web/src/grl/semantic/compileIo.ts
Normal file
380
kdl-wasm/web/src/grl/semantic/compileIo.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
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";
|
||||
|
||||
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 normalizedNumber(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 token = this.consumeNumber("Expected duration");
|
||||
return normalizedNumber(token);
|
||||
}
|
||||
|
||||
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();
|
||||
if (this.isKeywordLike(token, "wait") || this.isKeywordLike(token, "pulse")) {
|
||||
return true;
|
||||
}
|
||||
if (!this.isIoStartAtCurrent()) {
|
||||
return false;
|
||||
}
|
||||
const previous = tokens.at(-1);
|
||||
return previous ? token.range.start.line > previous.range.end.line : true;
|
||||
}
|
||||
|
||||
private isIoStartAtCurrent(): boolean {
|
||||
return this.peek().raw === "io" && this.maybePeek(1)?.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 normalizedNumber(token: GrlToken): number {
|
||||
if (token.kind !== "number") {
|
||||
throw ioError("GRL_NUMBER_EXPECTED", "Expected number");
|
||||
}
|
||||
return token.unit?.normalizedValue ?? token.value;
|
||||
}
|
||||
|
||||
function tokenSourceMap(token: GrlToken): MotionSourceMap {
|
||||
return {
|
||||
line: token.range.start.line,
|
||||
column: token.range.start.column
|
||||
};
|
||||
}
|
||||
|
||||
function ioError(code: string, message: string): KdlStructuredError {
|
||||
return new KdlStructuredError(code, message);
|
||||
}
|
||||
Reference in New Issue
Block a user