/** * Expression tree representation and parsing. */ import { ParseError } from './errors.js'; /** * Represents a prefix-notation expression tree. */ export class Expression { constructor( public readonly name: string, public readonly children: Expression[] = [] ) {} /** * Check structural equality of expressions. */ equals(other: Expression): boolean { if (this.name !== other.name) { return false; } if (this.children.length !== other.children.length) { return false; } return this.children.every((child, i) => child.equals(other.children[i])); } /** * Create a deep copy of this expression. */ copy(): Expression { return new Expression( this.name, this.children.map(c => c.copy()) ); } /** * Substitute all occurrences of varName with replacement. */ substitute(varName: string, replacement: Expression): Expression { if (this.name === varName && this.children.length === 0) { return replacement.copy(); } return new Expression( this.name, this.children.map(c => c.substitute(varName, replacement)) ); } /** * String representation of the expression. */ toString(): string { if (this.children.length === 0) { return this.name; } const childrenStr = this.children.map(c => c.toString()).join(', '); return `${this.name}(${childrenStr})`; } } /** * Parse a prefix notation expression, handling 'a : b' as var(a, b). */ export function parseExpression(text: string, lineNum: number): Expression { text = text.trim(); // Handle special case: "a : b" -> "var(a, b)" if (text.includes(':')) { // Find the colon that's not inside parentheses let depth = 0; let colonPos = -1; for (let i = 0; i < text.length; i++) { const ch = text[i]; if (ch === '(') { depth++; } else if (ch === ')') { depth--; } else if (ch === ':' && depth === 0) { colonPos = i; break; } } if (colonPos > 0) { const varName = text.substring(0, colonPos).trim(); const varType = text.substring(colonPos + 1).trim(); // Recursively parse to handle nested colons const typeExpr = parseExpression(varType, lineNum); return new Expression('var', [new Expression(varName), typeExpr]); } } // Find the opening parenthesis const parenPos = text.indexOf('('); if (parenPos === -1) { // Simple identifier with no children return new Expression(text); } // Extract name and arguments const exprName = text.substring(0, parenPos).trim(); // Find matching closing parenthesis if (!text.endsWith(')')) { throw new ParseError(`Line ${lineNum}: Mismatched parentheses in expression: ${text}`); } const argsText = text.substring(parenPos + 1, text.length - 1).trim(); // Parse comma-separated arguments (respecting nested parentheses) const args: string[] = []; if (argsText) { let currentArg = ''; let depth = 0; for (const ch of argsText) { if (ch === ',' && depth === 0) { args.push(currentArg.trim()); currentArg = ''; } else { if (ch === '(') { depth++; } else if (ch === ')') { depth--; } currentArg += ch; } } if (currentArg) { args.push(currentArg.trim()); } } // Recursively parse each argument const children = args.map(arg => parseExpression(arg, lineNum)); return new Expression(exprName, children); }