/** * Parser for .prf files. */ import { Block } from './block.js'; import { Expression, parseExpression } from './expression.js'; import { ParseError } from './errors.js'; interface ParsedLine { lineNum: number; level: number; text: string; } /** * Parse .prf content and return a list of top-level blocks. * This is the main parsing function that works in both browser and Node.js. */ export function parseContent(content: string): Block[] { const lines = content.split('\n'); // Parse lines with their indentation levels const parsedLines: ParsedLine[] = []; let indentUnit: string | null = null; for (let i = 0; i < lines.length; i++) { const lineNum = i + 1; let line = lines[i]; // Remove trailing newline/carriage return line = line.replace(/\r?\n?$/, ''); // Skip empty lines and comments if (!line.trim() || line.trim().startsWith('#')) { continue; } // Detect indentation let indentLevel = 0; const stripped = line.trimStart(); const indentStr = line.substring(0, line.length - stripped.length); if (indentStr) { // Detect indent unit from first indented line if (indentUnit === null) { indentUnit = indentStr; indentLevel = 1; } else { // Check if indentation is consistent if (indentStr.length % indentUnit.length !== 0) { throw new ParseError(`Line ${lineNum}: Inconsistent indentation`); } indentLevel = indentStr.length / indentUnit.length; } } parsedLines.push({ lineNum, level: indentLevel, text: stripped }); } // Build block tree return buildBlocks(parsedLines, 0, 0).blocks; } interface BuildResult { blocks: Block[]; nextIdx: number; } /** * Recursively build block tree from parsed lines. */ function buildBlocks(parsedLines: ParsedLine[], startIdx: number, expectedLevel: number): BuildResult { const blocks: Block[] = []; let idx = startIdx; while (idx < parsedLines.length) { const { lineNum, level, text } = parsedLines[idx]; // If we've gone back to a lower level, return to parent if (level < expectedLevel) { break; } // Skip lines at deeper levels (they'll be processed as children) if (level > expectedLevel) { throw new ParseError(`Line ${lineNum}: Unexpected indentation`); } // Parse the block const block = parseBlock(lineNum, text); // Process children at the next level if (idx + 1 < parsedLines.length) { const nextLevel = parsedLines[idx + 1].level; if (nextLevel > level) { const result = buildBlocks(parsedLines, idx + 1, level + 1); block.children = result.blocks; blocks.push(block); idx = result.nextIdx; continue; } } blocks.push(block); idx++; } return { blocks, nextIdx: idx }; } /** * Parse a single block line into a Block object. */ function parseBlock(lineNum: number, lineText: string): Block { // Split the line into tokens const tokens = tokenize(lineText, lineNum); if (tokens.length === 0) { throw new ParseError(`Line ${lineNum}: Empty block`); } const blockType = tokens[0]; const remainingTokens = tokens.slice(1); // Handle blocks with "name : type" pattern at the argument level // For suppose, assert, define, we-have, case: expect 3 tokens (name, :, type) let args: Expression[]; if (['suppose', 'assert', 'define', 'we-have', 'case'].includes(blockType)) { if (remainingTokens.length === 3 && remainingTokens[1] === ':') { const varName = remainingTokens[0]; const varTypeText = remainingTokens[2]; const varType = parseExpression(varTypeText, lineNum); args = [new Expression('var', [new Expression(varName), varType])]; } else { throw new ParseError(`Line ${lineNum}: '${blockType}' must have format 'name : type'`); } } else { // Parse the remaining tokens as expressions args = remainingTokens.map(token => parseExpression(token, lineNum)); } return new Block(lineNum, blockType, args); } /** * Tokenize a line, respecting parentheses and colons. */ function tokenize(line: string, lineNum: number): string[] { const tokens: string[] = []; let currentToken = ''; let depth = 0; for (const ch of line) { if (ch === '(' && depth === 0) { // Start of arguments currentToken += ch; depth++; } else if (ch === '(') { currentToken += ch; depth++; } else if (ch === ')') { currentToken += ch; depth--; } else if (ch === ':' && depth === 0) { // Colon outside parentheses - token separator and also a token if (currentToken.trim()) { tokens.push(currentToken.trim()); currentToken = ''; } tokens.push(':'); } else if (ch === ' ' && depth === 0) { // Space outside parentheses - token separator if (currentToken.trim()) { tokens.push(currentToken.trim()); currentToken = ''; } } else { currentToken += ch; } } if (currentToken.trim()) { tokens.push(currentToken.trim()); } if (depth !== 0) { throw new ParseError(`Line ${lineNum}: Mismatched parentheses`); } return tokens.filter(t => t.length > 0); }