UNPKG

kriti-lang

Version:

A TypeScript implementation of the Kriti templating language

1,241 lines (1,238 loc) 61.4 kB
'use strict'; // Kriti Template Language Lexer in TypeScript (No Regex) var TokenType; (function (TokenType) { // Literals TokenType["STRING"] = "STRING"; TokenType["NUMBER"] = "NUMBER"; TokenType["INTEGER"] = "INTEGER"; TokenType["BOOLEAN"] = "BOOLEAN"; TokenType["NULL"] = "NULL"; TokenType["IDENTIFIER"] = "IDENTIFIER"; // Keywords TokenType["IF"] = "IF"; TokenType["ELIF"] = "ELIF"; TokenType["ELSE"] = "ELSE"; TokenType["END"] = "END"; TokenType["RANGE"] = "RANGE"; TokenType["IN"] = "IN"; TokenType["NOT"] = "NOT"; TokenType["TRUE"] = "TRUE"; TokenType["FALSE"] = "FALSE"; // Operators TokenType["EQ"] = "EQ"; TokenType["NE"] = "NE"; TokenType["GT"] = "GT"; TokenType["LT"] = "LT"; TokenType["GTE"] = "GTE"; TokenType["LTE"] = "LTE"; TokenType["AND"] = "AND"; TokenType["OR"] = "OR"; TokenType["DEFAULT"] = "DEFAULT"; TokenType["ASSIGN"] = "ASSIGN"; // Punctuation TokenType["COLON"] = "COLON"; TokenType["DOT"] = "DOT"; TokenType["COMMA"] = "COMMA"; TokenType["QUESTION"] = "QUESTION"; TokenType["SQUOTE"] = "SQUOTE"; TokenType["UNDERSCORE"] = "UNDERSCORE"; TokenType["LPAREN"] = "LPAREN"; TokenType["RPAREN"] = "RPAREN"; TokenType["LBRACE"] = "LBRACE"; TokenType["RBRACE"] = "RBRACE"; TokenType["LBRACKET"] = "LBRACKET"; TokenType["RBRACKET"] = "RBRACKET"; TokenType["DQUOTE"] = "DQUOTE"; // Template delimiters TokenType["TEMPLATE_START"] = "TEMPLATE_START"; TokenType["TEMPLATE_END"] = "TEMPLATE_END"; // String template parts TokenType["STRING_LITERAL"] = "STRING_LITERAL"; // Special TokenType["EOF"] = "EOF"; TokenType["NEWLINE"] = "NEWLINE"; TokenType["WHITESPACE"] = "WHITESPACE"; TokenType["COMMENT"] = "COMMENT"; })(TokenType || (TokenType = {})); // Character class utility functions class CharUtils { static isDigit(char) { const code = char.charCodeAt(0); return code >= 48 && code <= 57; // '0' to '9' } static isLetter(char) { const code = char.charCodeAt(0); return (code >= 65 && code <= 90) || (code >= 97 && code <= 122); // 'A'-'Z' or 'a'-'z' } static isAlphaNumeric(char) { return this.isLetter(char) || this.isDigit(char); } static isIdentifierStart(char) { return this.isLetter(char) || char === '$'; } static isIdentifierChar(char) { return this.isAlphaNumeric(char) || char === '_' || char === '$' || char === '-'; } static isHexDigit(char) { const code = char.charCodeAt(0); return (code >= 48 && code <= 57) || // '0'-'9' (code >= 65 && code <= 70) || // 'A'-'F' (code >= 97 && code <= 102); // 'a'-'f' } static isWhitespace(char) { return char === ' ' || char === '\t' || char === '\r'; } static isLineTerminator(char) { return char === '\n'; } static isQuote(char) { return char === '"' || char === "'"; } static hexToDecimal(hex) { let result = 0; for (let i = 0; i < hex.length; i++) { const char = hex[i]; const code = char.charCodeAt(0); let digit; if (code >= 48 && code <= 57) { // '0'-'9' digit = code - 48; } else if (code >= 65 && code <= 70) { // 'A'-'F' digit = code - 65 + 10; } else if (code >= 97 && code <= 102) { // 'a'-'f' digit = code - 97 + 10; } else { return -1; // Invalid hex digit } result = result * 16 + digit; } return result; } } class LexerError extends Error { constructor(message, position) { super(`Lexer error at line ${position.line}, column ${position.column}: ${message}`); this.position = position; this.name = 'LexerError'; } } class KritiLexer { constructor(input) { this.position = 0; this.line = 1; this.column = 1; this.tokens = []; this.inStringTemplate = false; this.templateDepth = 0; this.input = input; } getCurrentPosition() { return { line: this.line, column: this.column, offset: this.position }; } peek(offset = 0) { const pos = this.position + offset; return pos < this.input.length ? this.input[pos] : ''; } advance() { if (this.position >= this.input.length) { return ''; } const char = this.input[this.position]; this.position++; if (char === '\n') { this.line++; this.column = 1; } else { this.column++; } return char; } skipWhitespace() { while (this.position < this.input.length && CharUtils.isWhitespace(this.peek())) { this.advance(); } } skipComment() { if (this.peek() === '#') { while (this.position < this.input.length && !CharUtils.isLineTerminator(this.peek())) { this.advance(); } } } readEscapeSequence() { const escaped = this.advance(); switch (escaped) { case 'n': return '\n'; case 't': return '\t'; case 'r': return '\r'; case 'b': return '\b'; case 'f': return '\f'; case '\\': return '\\'; case '"': return '"'; case '/': return '/'; case '{': return '{'; case 'u': { // Unicode escape sequence let hex = ''; for (let i = 0; i < 4; i++) { const char = this.peek(); if (!CharUtils.isHexDigit(char)) { throw new LexerError('Invalid unicode escape sequence', this.getCurrentPosition()); } hex += this.advance(); } const codePoint = CharUtils.hexToDecimal(hex); return String.fromCharCode(codePoint); } default: return escaped; } } readString() { let value = ''; this.advance(); // Skip opening quote while (this.position < this.input.length) { const char = this.peek(); if (char === '"') { this.advance(); // Skip closing quote break; } if (char === '\\') { this.advance(); // Skip backslash value += this.readEscapeSequence(); } else if (char === '{' && this.peek(1) === '{') { // This is a template interpolation, we'll handle it differently break; } else { value += this.advance(); } } return value; } readNumber() { let value = ''; let isDecimal = false; // Handle negative sign if (this.peek() === '-') { value += this.advance(); } // Read integer part while (CharUtils.isDigit(this.peek())) { value += this.advance(); } // Read decimal part if (this.peek() === '.' && CharUtils.isDigit(this.peek(1))) { isDecimal = true; value += this.advance(); // . while (CharUtils.isDigit(this.peek())) { value += this.advance(); } } // Read exponent part const expChar = this.peek(); if (expChar === 'e' || expChar === 'E') { isDecimal = true; value += this.advance(); // e or E const signChar = this.peek(); if (signChar === '+' || signChar === '-') { value += this.advance(); // + or - } while (CharUtils.isDigit(this.peek())) { value += this.advance(); } } return { value, isDecimal }; } readIdentifier() { let value = ''; // Handle optional $ prefix if (this.peek() === '$') { value += this.advance(); } // First character must be a letter if (CharUtils.isLetter(this.peek())) { value += this.advance(); } // Subsequent characters while (CharUtils.isIdentifierChar(this.peek())) { value += this.advance(); } return value; } readStringTemplatePart() { let value = ''; while (this.position < this.input.length) { const char = this.peek(); if (char === '"') { break; // End of string template } if (char === '{' && this.peek(1) === '{') { break; // Start of template expression } if (char === '\\') { this.advance(); // Skip backslash value += this.readEscapeSequence(); } else { value += this.advance(); } } return value; } createToken(type, value, start) { return { type, value, start, end: this.getCurrentPosition() }; } getKeywordType(value) { switch (value) { case 'if': return TokenType.IF; case 'elif': return TokenType.ELIF; case 'else': return TokenType.ELSE; case 'end': return TokenType.END; case 'null': return TokenType.NULL; case 'range': return TokenType.RANGE; case 'in': return TokenType.IN; case 'not': return TokenType.NOT; case 'true': return TokenType.TRUE; case 'false': return TokenType.FALSE; default: return null; } } matchTwoCharOperator() { const char1 = this.peek(); const char2 = this.peek(1); const twoChar = char1 + char2; switch (twoChar) { case '==': return { type: TokenType.EQ, value: '==' }; case '!=': return { type: TokenType.NE, value: '!=' }; case '>=': return { type: TokenType.GTE, value: '>=' }; case '<=': return { type: TokenType.LTE, value: '<=' }; case '&&': return { type: TokenType.AND, value: '&&' }; case '||': return { type: TokenType.OR, value: '||' }; case '??': return { type: TokenType.DEFAULT, value: '??' }; case ':=': return { type: TokenType.ASSIGN, value: ':=' }; case '{{': return { type: TokenType.TEMPLATE_START, value: '{{' }; case '}}': return { type: TokenType.TEMPLATE_END, value: '}}' }; default: return null; } } getSingleCharTokenType(char) { switch (char) { case ':': return TokenType.COLON; case '.': return TokenType.DOT; case ',': return TokenType.COMMA; case '?': return TokenType.QUESTION; case "'": return TokenType.SQUOTE; case '_': return TokenType.UNDERSCORE; case '(': return TokenType.LPAREN; case ')': return TokenType.RPAREN; case '{': return TokenType.LBRACE; case '}': return TokenType.RBRACE; case '[': return TokenType.LBRACKET; case ']': return TokenType.RBRACKET; case '>': return TokenType.GT; case '<': return TokenType.LT; default: return null; } } isNumberStart(char, nextChar) { return CharUtils.isDigit(char) || (char === '-' && CharUtils.isDigit(nextChar)); } tokenize() { this.tokens = []; while (this.position < this.input.length) { const start = this.getCurrentPosition(); const char = this.peek(); // Skip whitespace (except newlines) if (CharUtils.isWhitespace(char)) { this.skipWhitespace(); continue; } // Handle newlines if (CharUtils.isLineTerminator(char)) { this.advance(); continue; } // Skip comments if (char === '#') { this.skipComment(); continue; } // String templates if (char === '"') { this.inStringTemplate = true; this.advance(); // Skip opening quote this.tokens.push(this.createToken(TokenType.DQUOTE, '"', start)); // Read string template parts while (this.position < this.input.length && this.inStringTemplate) { const partStart = this.getCurrentPosition(); if (this.peek() === '"') { // End of string template this.advance(); this.tokens.push(this.createToken(TokenType.DQUOTE, '"', partStart)); this.inStringTemplate = false; break; } if (this.peek() === '{' && this.peek(1) === '{') { // Template expression start this.advance(); this.advance(); this.tokens.push(this.createToken(TokenType.TEMPLATE_START, '{{', partStart)); this.templateDepth++; // Recursively tokenize the expression inside this.tokenizeExpression(); continue; } // Regular string content const content = this.readStringTemplatePart(); if (content) { this.tokens.push(this.createToken(TokenType.STRING_LITERAL, content, partStart)); } } continue; } // Check for two-character operators first const twoCharOp = this.matchTwoCharOperator(); if (twoCharOp) { this.advance(); this.advance(); this.tokens.push(this.createToken(twoCharOp.type, twoCharOp.value, start)); // Special handling for template starts if (twoCharOp.type === TokenType.TEMPLATE_START) { this.templateDepth++; } else if (twoCharOp.type === TokenType.TEMPLATE_END) { this.templateDepth--; } continue; } // Single-character tokens const singleCharType = this.getSingleCharTokenType(char); if (singleCharType) { this.advance(); this.tokens.push(this.createToken(singleCharType, char, start)); continue; } // Numbers if (this.isNumberStart(char, this.peek(1))) { const { value, isDecimal } = this.readNumber(); const tokenType = isDecimal ? TokenType.NUMBER : TokenType.INTEGER; this.tokens.push(this.createToken(tokenType, value, start)); continue; } // Identifiers and keywords if (CharUtils.isIdentifierStart(char)) { const value = this.readIdentifier(); const keywordType = this.getKeywordType(value); if (keywordType) { this.tokens.push(this.createToken(keywordType, value, start)); } else { this.tokens.push(this.createToken(TokenType.IDENTIFIER, value, start)); } continue; } // If we reach here, it's an unexpected character throw new LexerError(`Unexpected character: '${char}' (code: ${char.charCodeAt(0)})`, start); } // Add EOF token this.tokens.push(this.createToken(TokenType.EOF, '', this.getCurrentPosition())); return this.tokens; } tokenizeExpression() { // Handle tokenizing expressions inside template blocks let braceDepth = 1; while (this.position < this.input.length && braceDepth > 0) { const start = this.getCurrentPosition(); const char = this.peek(); // Skip whitespace if (CharUtils.isWhitespace(char) || CharUtils.isLineTerminator(char)) { if (CharUtils.isLineTerminator(char)) { this.advance(); } else { this.skipWhitespace(); } continue; } // Skip comments if (char === '#') { this.skipComment(); continue; } // Check for template end if (char === '}' && this.peek(1) === '}') { this.advance(); this.advance(); this.tokens.push(this.createToken(TokenType.TEMPLATE_END, '}}', start)); braceDepth--; this.templateDepth--; break; } // Handle nested template expressions if (char === '{' && this.peek(1) === '{') { this.advance(); this.advance(); this.tokens.push(this.createToken(TokenType.TEMPLATE_START, '{{', start)); braceDepth++; this.templateDepth++; continue; } // Handle strings inside expressions if (char === '"') { const content = this.readString(); this.tokens.push(this.createToken(TokenType.STRING_LITERAL, content, start)); continue; } // Handle all other token types if (!this.tokenizeNextToken()) { throw new LexerError(`Unable to tokenize character: '${char}' (code: ${char.charCodeAt(0)})`, start); } } } tokenizeNextToken() { const start = this.getCurrentPosition(); const char = this.peek(); // Two-character operators const twoCharOp = this.matchTwoCharOperator(); if (twoCharOp) { this.advance(); this.advance(); this.tokens.push(this.createToken(twoCharOp.type, twoCharOp.value, start)); return true; } // Single-character tokens const singleCharType = this.getSingleCharTokenType(char); if (singleCharType) { this.advance(); this.tokens.push(this.createToken(singleCharType, char, start)); return true; } // Numbers if (this.isNumberStart(char, this.peek(1))) { const { value, isDecimal } = this.readNumber(); const tokenType = isDecimal ? TokenType.NUMBER : TokenType.INTEGER; this.tokens.push(this.createToken(tokenType, value, start)); return true; } // Identifiers and keywords if (CharUtils.isIdentifierStart(char)) { const value = this.readIdentifier(); const keywordType = this.getKeywordType(value); if (keywordType) { this.tokens.push(this.createToken(keywordType, value, start)); } else { this.tokens.push(this.createToken(TokenType.IDENTIFIER, value, start)); } return true; } return false; } } function tokenizeKriti(input) { const lexer = new KritiLexer(input); return lexer.tokenize(); } // Kriti AST Node Types and Pratt Parser // Precedence levels for Pratt parser var Precedence; (function (Precedence) { Precedence[Precedence["LOWEST"] = 0] = "LOWEST"; Precedence[Precedence["LOGICAL_OR"] = 10] = "LOGICAL_OR"; Precedence[Precedence["LOGICAL_AND"] = 20] = "LOGICAL_AND"; Precedence[Precedence["IN"] = 30] = "IN"; Precedence[Precedence["EQUALITY"] = 40] = "EQUALITY"; Precedence[Precedence["RELATIONAL"] = 50] = "RELATIONAL"; Precedence[Precedence["DEFAULTING"] = 60] = "DEFAULTING"; Precedence[Precedence["UNARY"] = 70] = "UNARY"; Precedence[Precedence["ACCESS"] = 80] = "ACCESS"; Precedence[Precedence["PRIMARY"] = 90] = "PRIMARY"; // literals, variables, function calls })(Precedence || (Precedence = {})); // Parser Error Types class ParseError extends Error { constructor(message, position, token) { super(`Parse error at line ${position.line}, column ${position.column}: ${message}`); this.position = position; this.token = token; this.name = 'ParseError'; } } // Parser Class using Pratt parsing technique class KritiParser { constructor(tokens) { this.position = 0; // Pratt parser function tables this.prefixParseFns = new Map(); this.infixParseFns = new Map(); this.precedences = new Map(); this.tokens = tokens; this.current = this.tokens[0] || this.createEOFToken(); this.initializeParseFunctions(); this.initializePrecedences(); } createEOFToken() { return { type: TokenType.EOF, value: '', start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 1, offset: 0 } }; } // Initialize precedence table initializePrecedences() { this.precedences.set(TokenType.OR, Precedence.LOGICAL_OR); this.precedences.set(TokenType.AND, Precedence.LOGICAL_AND); this.precedences.set(TokenType.IN, Precedence.IN); this.precedences.set(TokenType.EQ, Precedence.EQUALITY); this.precedences.set(TokenType.NE, Precedence.EQUALITY); this.precedences.set(TokenType.GT, Precedence.RELATIONAL); this.precedences.set(TokenType.LT, Precedence.RELATIONAL); this.precedences.set(TokenType.GTE, Precedence.RELATIONAL); this.precedences.set(TokenType.LTE, Precedence.RELATIONAL); this.precedences.set(TokenType.DEFAULT, Precedence.DEFAULTING); this.precedences.set(TokenType.DOT, Precedence.ACCESS); this.precedences.set(TokenType.LBRACKET, Precedence.ACCESS); this.precedences.set(TokenType.QUESTION, Precedence.ACCESS); } // Initialize prefix and infix parsing functions initializeParseFunctions() { // Prefix parsers (nud - null denotation) this.prefixParseFns.set(TokenType.IDENTIFIER, this.parseIdentifierOrFunctionCall.bind(this)); this.prefixParseFns.set(TokenType.STRING_LITERAL, this.parseStringLiteral.bind(this)); this.prefixParseFns.set(TokenType.NUMBER, this.parseNumber.bind(this)); this.prefixParseFns.set(TokenType.INTEGER, this.parseInteger.bind(this)); this.prefixParseFns.set(TokenType.TRUE, this.parseBoolean.bind(this)); this.prefixParseFns.set(TokenType.FALSE, this.parseBoolean.bind(this)); this.prefixParseFns.set(TokenType.NULL, this.parseNull.bind(this)); this.prefixParseFns.set(TokenType.LBRACKET, this.parseArray.bind(this)); this.prefixParseFns.set(TokenType.LBRACE, this.parseObject.bind(this)); this.prefixParseFns.set(TokenType.DQUOTE, this.parseStringTemplate.bind(this)); this.prefixParseFns.set(TokenType.TEMPLATE_START, this.parseTemplateExpression.bind(this)); this.prefixParseFns.set(TokenType.NOT, this.parseUnaryExpression.bind(this)); this.prefixParseFns.set(TokenType.LPAREN, this.parseGroupedExpression.bind(this)); // Infix parsers (led - left denotation) this.infixParseFns.set(TokenType.OR, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.AND, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.IN, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.EQ, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.NE, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.GT, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.LT, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.GTE, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.LTE, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.DEFAULT, this.parseBinaryExpression.bind(this)); this.infixParseFns.set(TokenType.DOT, this.parseFieldAccess.bind(this)); this.infixParseFns.set(TokenType.LBRACKET, this.parseComputedAccess.bind(this)); this.infixParseFns.set(TokenType.QUESTION, this.parseOptionalChain.bind(this)); } // Get precedence for current token currentPrecedence() { return this.precedences.get(this.current.type) || Precedence.LOWEST; } // Check if a token type should terminate expression parsing isTerminatingToken(tokenType) { return [ TokenType.EOF, TokenType.TEMPLATE_END, TokenType.RPAREN, TokenType.RBRACKET, TokenType.RBRACE, TokenType.COMMA, TokenType.COLON, TokenType.DQUOTE, TokenType.SQUOTE, TokenType.ASSIGN, // := TokenType.IF, TokenType.ELIF, TokenType.ELSE, TokenType.END, TokenType.RANGE ].includes(tokenType); } advance() { const previous = this.current; if (this.position < this.tokens.length - 1) { this.position++; this.current = this.tokens[this.position]; } return previous; } peek(offset = 1) { const pos = this.position + offset; return pos < this.tokens.length ? this.tokens[pos] : this.createEOFToken(); } match(...types) { return types.includes(this.current.type); } consume(type, message) { if (this.current.type === type) { const token = this.current; this.advance(); return token; } throw new ParseError(message || `Expected ${type}, got ${this.current.type}`, this.current.start, this.current); } createNode(type, start, end, props = {}) { return { type, start, end, ...props }; } // Main parsing entry point parse() { const expr = this.parseExpression(); if (this.current.type !== TokenType.EOF) { throw new ParseError('Unexpected token after expression', this.current.start, this.current); } return expr; } // Core Pratt parsing expression method parseExpression(precedence = Precedence.LOWEST) { const prefixFn = this.prefixParseFns.get(this.current.type); if (!prefixFn) { throw new ParseError(`No prefix parse function found for token type: ${this.current.type}`, this.current.start, this.current); } let left = prefixFn(); // Pratt parsing loop: keep consuming tokens with higher precedence // Check current token for termination and next token for precedence while (!this.isTerminatingToken(this.current.type) && precedence < this.currentPrecedence()) { const infixFn = this.infixParseFns.get(this.current.type); if (!infixFn) { return left; } left = infixFn(left); } return left; } // Prefix parsing functions (nud - null denotation) parseIdentifierOrFunctionCall() { const start = this.current.start; if (!this.match(TokenType.IDENTIFIER)) { throw new ParseError(`Expected identifier`, this.current.start, this.current); } // Check if this is a function call if (this.peek().type === TokenType.LPAREN) { const name = this.advance(); this.consume(TokenType.LPAREN, 'Expected "("'); const argument = this.parseExpression(); const end = this.consume(TokenType.RPAREN, 'Expected ")"'); return this.createNode('FunctionCall', name.start, end.end, { name: name.value, argument }); } // Regular identifier/variable const token = this.advance(); return this.createNode('Variable', start, token.end, { name: token.value }); } parseStringLiteral() { const start = this.current.start; const token = this.advance(); return this.createNode('Literal', start, token.end, { valueType: 'string', value: token.value }); } parseNumber() { const start = this.current.start; const token = this.advance(); return this.createNode('Literal', start, token.end, { valueType: 'number', value: parseFloat(token.value) }); } parseInteger() { const start = this.current.start; const token = this.advance(); return this.createNode('Literal', start, token.end, { valueType: 'integer', value: parseInt(token.value, 10) }); } parseBoolean() { const start = this.current.start; const token = this.advance(); return this.createNode('Literal', start, token.end, { valueType: 'boolean', value: token.type === TokenType.TRUE }); } parseNull() { const start = this.current.start; const token = this.advance(); return this.createNode('Literal', start, token.end, { valueType: 'null', value: null }); } parseUnaryExpression() { const start = this.advance(); const operand = this.parseExpression(Precedence.UNARY); return this.createNode('UnaryExpression', start.start, operand.end, { operator: 'not', operand }); } parseGroupedExpression() { this.advance(); // consume ( const expr = this.parseExpression(); this.consume(TokenType.RPAREN, 'Expected ")"'); return expr; } // Handle template expressions - needs special logic parseTemplateExpression() { const nextToken = this.peek(); if (nextToken.type === TokenType.RANGE) { return this.parseRange(); } else if (nextToken.type === TokenType.IF) { return this.parseConditional(); } else { const start = this.advance().start; // consume {{ const expression = this.parseExpression(); const end = this.consume(TokenType.TEMPLATE_END, 'Expected "}}"').end; return this.createNode('TemplateExpression', start, end, { expression }); } } // Infix parsing functions (led - left denotation) parseBinaryExpression(left) { const operatorToken = this.current; const precedence = this.currentPrecedence(); // Map token types to operator strings const operatorMap = { [TokenType.OR]: '||', [TokenType.AND]: '&&', [TokenType.IN]: 'in', [TokenType.EQ]: '==', [TokenType.NE]: '!=', [TokenType.GT]: '>', [TokenType.LT]: '<', [TokenType.GTE]: '>=', [TokenType.LTE]: '<=', [TokenType.DEFAULT]: '??' }; const operator = operatorMap[operatorToken.type]; if (!operator) { throw new ParseError(`Unknown binary operator: ${operatorToken.type}`, operatorToken.start, operatorToken); } this.advance(); const right = this.parseExpression(precedence); return this.createNode('BinaryExpression', left.start, right.end, { operator, left, right }); } parseFieldAccess(left) { this.advance(); // consume . const field = this.consume(TokenType.IDENTIFIER, 'Expected field name after "."'); return this.createNode('FieldAccess', left.start, field.end, { object: left, field: field.value, computed: false, optional: false }); } parseComputedAccess(left) { this.advance(); // consume [ let fieldExpr; if (this.match(TokenType.SQUOTE)) { this.advance(); // Accept either STRING_LITERAL or IDENTIFIER between single quotes const stringToken = this.match(TokenType.STRING_LITERAL) ? this.consume(TokenType.STRING_LITERAL, 'Expected string literal') : this.consume(TokenType.IDENTIFIER, 'Expected string literal or identifier'); this.consume(TokenType.SQUOTE, 'Expected closing single quote'); fieldExpr = this.createNode('Literal', stringToken.start, stringToken.end, { valueType: 'string', value: stringToken.value }); } else { fieldExpr = this.parseExpression(Precedence.LOWEST); } const end = this.consume(TokenType.RBRACKET, 'Expected "]"'); return this.createNode('FieldAccess', left.start, end.end, { object: left, field: fieldExpr, computed: true, optional: false }); } parseOptionalChain(left) { this.advance(); // consume ? // Start an optional chain const chain = []; let lastPosition = this.current.start || left.end; // Continue parsing access operations for the optional chain while (this.match(TokenType.DOT, TokenType.LBRACKET)) { if (this.match(TokenType.DOT)) { this.advance(); const field = this.consume(TokenType.IDENTIFIER, 'Expected field name after "."'); chain.push({ type: 'field', value: field.value }); lastPosition = field.end; } else if (this.match(TokenType.LBRACKET)) { this.advance(); let value; if (this.match(TokenType.SQUOTE)) { this.advance(); // Accept either STRING_LITERAL or IDENTIFIER between single quotes const stringToken = this.match(TokenType.STRING_LITERAL) ? this.consume(TokenType.STRING_LITERAL, 'Expected string literal') : this.consume(TokenType.IDENTIFIER, 'Expected string literal or identifier'); this.consume(TokenType.SQUOTE, 'Expected closing single quote'); value = stringToken.value; } else { value = this.parseExpression(); } const end = this.consume(TokenType.RBRACKET, 'Expected "]"'); chain.push({ type: 'computed', value }); lastPosition = end.end; } } return this.createNode('OptionalChain', left.start, lastPosition, { object: left, chain }); } // Keep existing specialized parsing methods but update them to use new parseExpression parseArray() { const start = this.advance().start; // consume [ const elements = []; if (!this.match(TokenType.RBRACKET)) { do { elements.push(this.parseExpression()); } while (this.match(TokenType.COMMA) && this.advance()); } const end = this.consume(TokenType.RBRACKET, 'Expected "]"').end; return this.createNode('Array', start, end, { elements }); } parseObject() { const start = this.advance().start; // consume { const properties = []; if (!this.match(TokenType.RBRACE)) { do { const key = this.parseStringTemplate(); // Object keys are string templates this.consume(TokenType.COLON, 'Expected ":"'); const value = this.parseExpression(); properties.push({ key, value }); } while (this.match(TokenType.COMMA) && this.advance()); } const end = this.consume(TokenType.RBRACE, 'Expected "}"').end; return this.createNode('Object', start, end, { properties }); } parseStringTemplate() { const start = this.advance().start; // consume " const parts = []; while (!this.match(TokenType.DQUOTE) && !this.match(TokenType.EOF)) { if (this.match(TokenType.STRING_LITERAL)) { const token = this.advance(); parts.push(this.createNode('Literal', token.start, token.end, { valueType: 'string', value: token.value })); } else if (this.match(TokenType.TEMPLATE_START)) { this.advance(); const expr = this.parseExpression(); this.consume(TokenType.TEMPLATE_END, 'Expected "}}"'); parts.push(expr); } else { break; } } const end = this.consume(TokenType.DQUOTE, 'Expected closing "\""').end; return this.createNode('StringTemplate', start, end, { parts }); } parseRange() { const start = this.advance().start; // consume {{ this.consume(TokenType.RANGE, 'Expected "range"'); let index = null; if (this.match(TokenType.IDENTIFIER)) { index = this.advance().value; this.consume(TokenType.COMMA, 'Expected ","'); } else if (this.match(TokenType.UNDERSCORE)) { this.advance(); this.consume(TokenType.COMMA, 'Expected ","'); } const variable = this.consume(TokenType.IDENTIFIER, 'Expected variable name').value; this.consume(TokenType.ASSIGN, 'Expected ":="'); const iterable = this.parseExpression(); this.consume(TokenType.TEMPLATE_END, 'Expected "}}"'); const body = this.parseExpression(); this.consume(TokenType.TEMPLATE_START, 'Expected "{{"'); this.consume(TokenType.END, 'Expected "end"'); const end = this.consume(TokenType.TEMPLATE_END, 'Expected "}}"').end; return this.createNode('Range', start, end, { index, variable, iterable, body }); } parseConditional() { const start = this.advance().start; // consume {{ this.consume(TokenType.IF, 'Expected "if"'); const condition = this.parseExpression(); this.consume(TokenType.TEMPLATE_END, 'Expected "}}"'); const thenExpression = this.parseExpression(); const elifs = []; while (this.match(TokenType.TEMPLATE_START) && this.peek().type === TokenType.ELIF) { this.advance(); this.consume(TokenType.ELIF, 'Expected "elif"'); const elifCondition = this.parseExpression(); this.consume(TokenType.TEMPLATE_END, 'Expected "}}"'); const elifExpression = this.parseExpression(); elifs.push({ condition: elifCondition, expression: elifExpression }); } this.consume(TokenType.TEMPLATE_START, 'Expected "{{"'); this.consume(TokenType.ELSE, 'Expected "else"'); this.consume(TokenType.TEMPLATE_END, 'Expected "}}"'); const elseExpression = this.parseExpression(); this.consume(TokenType.TEMPLATE_START, 'Expected "{{"'); this.consume(TokenType.END, 'Expected "end"'); const end = this.consume(TokenType.TEMPLATE_END, 'Expected "}}"').end; return this.createNode('Conditional', start, end, { condition, thenExpression, elifs, elseExpression }); } } // Utility functions function parseKriti(tokens) { const parser = new KritiParser(tokens); return parser.parse(); } // Kriti Template Language Interpreter/Evaluator // Runtime errors class RuntimeError extends Error { constructor(message, position, node) { super(position ? `Runtime error at line ${position.line}, column ${position.column}: ${message}` : `Runtime error: ${message}`); this.position = position; this.node = node; this.name = 'RuntimeError'; } } // Built-in functions class BuiltinFunctions { static createDefaultFunctions() { const functions = new Map(); // Logical not function functions.set('not', (value) => { if (typeof value === 'boolean') { return !value; } throw new RuntimeError(`Cannot apply 'not' to non-boolean value: ${typeof value}`); }); // Empty function - checks if value is "empty" functions.set('empty', (value) => { if (value === null || value === undefined) return true; if (typeof value === 'string') return value.trim() === ''; if (typeof value === 'number') return value === 0; if (Array.isArray(value)) return value.length === 0; if (typeof value === 'object') return Object.keys(value).length === 0; return false; }); // Length/size function functions.set('length', (value) => { if (typeof value === 'string') return value.length; if (Array.isArray(value)) return value.length; if (value && typeof value === 'object') return Object.keys(value).length; if (typeof value === 'number') return value; throw new RuntimeError(`Cannot get length of: ${typeof value}`); }); functions.set('size', functions.get('length')); // Alias for length // String manipulation functions functions.set('toUpper', (value) => { if (typeof value === 'string') return value.toUpperCase(); throw new RuntimeError(`Cannot convert non-string to uppercase: ${typeof value}`); }); functions.set('toLower', (value) => { if (typeof value === 'string') return value.toLowerCase(); throw new RuntimeError(`Cannot convert non-string to lowercase: ${typeof value}`); }); functions.set('toCaseFold', (value) => { if (typeof value === 'string') return value.toLowerCase(); // Simplified case folding throw new RuntimeError(`Cannot case fold non-string: ${typeof value}`); }); functions.set('toTitle', (value) => { if (typeof value === 'string') { return value.replace(/\b\w+/g, word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); } throw new RuntimeError(`Cannot convert non-string to title case: ${typeof value}`); }); // Array functions functions.set('head', (value) => { if (Array.isArray(value)) { if (value.length === 0) throw new RuntimeError('Cannot get head of empty array'); return value[0]; } if (typeof value === 'string') { if (value.length === 0) throw new RuntimeError('Cannot get head of empty string'); return value.charAt(0); } throw new RuntimeError(`Cannot get head of: ${typeof value}`); }); functions.set('tail', (value) => { if (Array.isArray(value)) { return value.slice(1); } if (typeof value === 'string') { return value.slice(1); } throw new RuntimeError(`Cannot get tail of: ${typeof value}`); }); functions.set('inverse', (value) => { if (Array.isArray(value)) return [...value].reverse(); if (typeof value === 'string') return value.split('').reverse().join(''); if (typeof value === 'number') return 1 / value; if (typeof value === 'boolean') return !value; if (value === null) return null; if (typeof value === 'object') return value; // Objects returned as-is throw new RuntimeError(`Cannot get inverse of: ${typeof value}`); }); functions.set('concat', (value) => { if (Array.isArray(value)) { // Check if all items are objects - if so, merge them if (value.every(item => item && typeof item === 'object' && !Array.isArray(item))) { const result = {}; for (const item of value) { Object.assign(result, item); } return result; } // Check if all items are strings - if so, concatenate them if (value.every(item => typeof item === 'string')) { return value.join(''); } // Otherwise, concatenate arrays const result = []; for (const item of value) { if (Array.isArray(item)) { result.push(...item); } else { result.push(item); } } return result; } throw new RuntimeError(`Cannot concat non-array: ${typeof value}`); }); // Object functions functions.set('toPairs', (value) => { if (value && typeof value === 'object' && !Array.isArray(value)) { return Object.entries(value).map(([k, v]) => [k, v]); } throw new RuntimeError(`Cannot convert non-object to pairs: ${typeof value}`); }); functions.set('fromPairs', (value) => { if (Array.isArray(value)) { const result = {}; for (const pair of value) { if (Array.isArray(pair) && pair.length === 2) { const [key, val] = pair; if (typeof key === 'string') { result[key] = val; } else { throw new RuntimeError('Object keys must be strings'); } } else { throw new RuntimeError('fromPairs requires array of [key, value] pairs'); } } return result; } throw new RuntimeError(`Cannot convert non-array to object: ${typeof value}`); }); functions.set('removeNulls', (value) => { if (Array.isArray(value)) { return value.filter(item => item !== null); } throw new RuntimeError(`Cannot remove nulls from non-array: ${typeof value}`); }); functions.set('escapeUri', (value) => { if (typeof value === 'string') { return encodeURIComponent(value); } throw new RuntimeError(`Cannot escape non-string URI: ${typeof value}`); }); return functions; } } // Main interpreter class class KritiInterpreter { constructor(variables = {}, customFunctions = new Map()) { const builtinFunctions = BuiltinFunctions.createDefaultFunctions(); // Merge custom functions with built-ins (custom functions override built-ins) const allFunctions = new Map([...builtinFunctions, ...customFunctions]); this.context = { variables: new Map(Object.entries(variables)), functions: allFunctions }; } evaluate(node) { try { return this.evaluateNode(node, this.context); } catch (error) { if (error instanceof RuntimeError) { throw error; } throw new RuntimeError(`Unexpected error during evaluation: ${error}`, node.start, node); } } evaluateNode(node, context) { switch (node.type) { case 'Literal': return this.evaluateLiteral(node); case 'Variable': return this.evaluateVariable(node, context); case 'BinaryExpression': return this.evaluateBinaryExpression(node, context); case 'UnaryExpression': return this.evaluateUnaryExpression(node, context); case 'FieldAccess': return this.evaluateFieldAccess(node, context); case 'OptionalChain': return this.evaluateOptionalChain(node, context); case 'FunctionCall': return this.evaluateFunctionCall(node, context); case 'Array': return this.evaluateArray(node, context); case 'Object': return this.evaluateObject(node, context); case 'StringTemplate': return this.evaluateStringTemplate(node, context); case 'Conditional': return this.evaluateConditional(node, context); case 'Range': return this.evaluateRange(node, context); case 'TemplateExpression': return this.evaluateNode(node.expression, context); default: throw new RuntimeError(`Unknown node type: ${node.type}`, node.start, node); } } evaluateLiteral(node) { return node.value; } evaluateVariable(node, context) { let currentContext = context; while (currentContext) { if (currentContext.variables.has(node.name)) { return currentContext.variables.get(node.name); } currentContext = currentContext.parent; } throw new RuntimeError(`Variable '${node.name}' not found in scope`, node.start, node); } evalu