1/**
2 * Expression tree representation and parsing.
3 */
5import { ParseError } from './errors.js';
7/**
8 * Represents a prefix-notation expression tree.
9 */
10export class Expression {
11 constructor(
12 public readonly name: string,
13 public readonly children: Expression[] = []
14 ) {}
16 /**
17 * Check structural equality of expressions.
18 */
19 equals(other: Expression): boolean {
20 if (this.name !== other.name) {
21 return false;
22 }
23 if (this.children.length !== other.children.length) {
24 return false;
25 }
26 return this.children.every((child, i) => child.equals(other.children[i]));
27 }
29 /**
30 * Create a deep copy of this expression.
31 */
32 copy(): Expression {
33 return new Expression(
34 this.name,
35 this.children.map(c => c.copy())
36 );
37 }
39 /**
40 * Substitute all occurrences of varName with replacement.
41 */
42 substitute(varName: string, replacement: Expression): Expression {
43 if (this.name === varName && this.children.length === 0) {
44 return replacement.copy();
45 }
46 return new Expression(
47 this.name,
48 this.children.map(c => c.substitute(varName, replacement))
49 );
50 }
52 /**
53 * String representation of the expression.
54 */
55 toString(): string {
56 if (this.children.length === 0) {
57 return this.name;
58 }
59 const childrenStr = this.children.map(c => c.toString()).join(', ');
60 return `${this.name}(${childrenStr})`;
61 }
62}
64/**
65 * Parse a prefix notation expression, handling 'a : b' as var(a, b).
66 */
67export function parseExpression(text: string, lineNum: number): Expression {
68 text = text.trim();
70 // Handle special case: "a : b" -> "var(a, b)"
71 if (text.includes(':')) {
72 // Find the colon that's not inside parentheses
73 let depth = 0;
74 let colonPos = -1;
75 for (let i = 0; i < text.length; i++) {
76 const ch = text[i];
77 if (ch === '(') {
78 depth++;
79 } else if (ch === ')') {
80 depth--;
81 } else if (ch === ':' && depth === 0) {
82 colonPos = i;
83 break;
84 }
85 }
87 if (colonPos > 0) {
88 const varName = text.substring(0, colonPos).trim();
89 const varType = text.substring(colonPos + 1).trim();
90 // Recursively parse to handle nested colons
91 const typeExpr = parseExpression(varType, lineNum);
92 return new Expression('var', [new Expression(varName), typeExpr]);
93 }
94 }
96 // Find the opening parenthesis
97 const parenPos = text.indexOf('(');
99 if (parenPos === -1) {
100 // Simple identifier with no children
101 return new Expression(text);
102 }
104 // Extract name and arguments
105 const exprName = text.substring(0, parenPos).trim();
107 // Find matching closing parenthesis
108 if (!text.endsWith(')')) {
109 throw new ParseError(`Line ${lineNum}: Mismatched parentheses in expression: ${text}`);
110 }
112 const argsText = text.substring(parenPos + 1, text.length - 1).trim();
114 // Parse comma-separated arguments (respecting nested parentheses)
115 const args: string[] = [];
116 if (argsText) {
117 let currentArg = '';
118 let depth = 0;
119 for (const ch of argsText) {
120 if (ch === ',' && depth === 0) {
121 args.push(currentArg.trim());
122 currentArg = '';
123 } else {
124 if (ch === '(') {
125 depth++;
126 } else if (ch === ')') {
127 depth--;
128 }
129 currentArg += ch;
130 }
131 }
133 if (currentArg) {
134 args.push(currentArg.trim());
135 }
136 }
138 // Recursively parse each argument
139 const children = args.map(arg => parseExpression(arg, lineNum));
141 return new Expression(exprName, children);
142}