UNPKG

kriti-lang

Version:

A TypeScript implementation of the Kriti templating language

1,539 lines 61.4 kB
// 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);
    }
    evaluateBinaryExpression(node, context) {
        const left = this.evaluateNode(node.left, context);
        const right = this.evaluateNode(node.right, context);
        switch (node.operator) {
            case '==':
                return this.deepEqual(left, right);
            case '!=':
                return !this.deepEqual(left, right);
            case '>':
                return this.compare(left, right, node) > 0;
            case '<':
                return this.compare(left, right, node) < 0;
            case '>=':
                return this.compare(left, right, node) >= 0;
            case '<=':
                return this.compare(left, right, node) <= 0;
            case '&&':
                return this.isTruthy(left) && this.isTruthy(right);
            case '||':
                return this.isTruthy(left) || this.isTruthy(right);
            case 'in':
                return this.evaluateInOperator(left, right, node);
            case '??':
                return left !== null && left !== undefined ? left : right;
            default:
                throw new RuntimeError(`Unknown binary operator: ${node.operator}`, node.start, node);
        }
    }
    evaluateUnaryExpression(node, context) {
        const operand = this.evaluateNode(node.operand, context);
        switch (node.operator) {
            case 'not':
                return !this.isTruthy(operand);
            default:
                throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node.start, node);
        }
    }
    evaluateFieldAccess(node, context) {
        const object = this.evaluateNode(node.object, context);
        if (object === null || object === undefined) {
            throw new RuntimeError('Cannot access property of null or undefined', node.start, node);
        }
        let key;
        if (typeof node.field === 'string') {
            key = node.field;
        }
        else {
            const fieldValue = this.evaluateNode(node.field, context);
            if (typeof fieldValue === 'string' || typeof fieldValue === 'number') {
                key = fieldValue;
            }
            else {
                throw new RuntimeError(`Field access key must be string or number, got: ${typeof fieldValue}`, node.start, node);
            }
        }
        if (Array.isArray(object)) {
            if (typeof key === 'number') {
                if (key < 0 || key >= object.length || !Number.isInteger(key)) {
                    throw new RuntimeError(`Array index out of bounds: ${key}`, node.start, node);
                }
                return object[key];
            }
            else if (key === 'length') {
                return object.length;
            }
            else {
                throw new RuntimeError(`Invalid array property: ${key}`, node.start, node);
            }
        }
        if (typeof object === 'object') {
            const result = object[key.toString()];
            if (result === undefined) {
                throw new RuntimeError(`Property '${key}' not found on object`, node.start, node);
            }
            return result;
        }
        throw new RuntimeError(`Cannot access property of ${typeof object}`, node.start, node);
    }
    evaluateOptionalChain(node, context) {
        let current;
        try {
            current = this.evaluateNode(node.object, context);
        }
        catch (error) {
            // If the base object evaluation fails (e.g., variable not found), return null for optional access
            if (error instanceof RuntimeError) {
                return null;
            }
            throw error;
        }
        for (const link of node.chain) {
            if (current === null || current === undefined) {
                return null;
            }
            try {
                if (link.type === 'field') {
                    if (typeof current === 'object' && !Array.isArray(current)) {
                        current = current[link.value];
                    }
                    else {
                        return null;
                    }
                }
                else { // computed
                    const key = typeof link.value === 'string' ?
                        link.value :
                        this.evaluateNode(link.value, context);
                    if (Array.isArray(current)) {
                        if (typeof key === 'number' && Number.isInteger(key) && key >= 0 && key < current.length) {
                            current = current[key];
                        }
                        else {
                            return null;
                        }
                    }
                    else if (typeof current === 'object') {
                        current = current[key.toString()];
                    }
                    else {
                        return null;
                    }
                }
            }
            catch {
                return null;
            }
        }
        return current === undefined ? null : current;
    }
    evaluateFunctionCall(node, context) {
        const func = context.functions.get(node.name);
        if (!func) {
            throw new RuntimeError(`Function '${node.name}' not found`, node.start, node);
        }
        const argument = this.evaluateNode(node.argument, context);
        try {
            return func(argument, context);
        }
        catch (error) {
            if (error instanceof RuntimeError) {
                throw error;
            }
            throw new RuntimeError(`Error in function '${node.name}': ${error}`, node.start, node);
        }
    }
    evaluateArray(node, context) {
        return node.elements.map(element => this.evaluateNode(element, context));
    }
    evaluateObject(node, context) {
        const result = {};
        for (const prop of node.properties) {
            const key = this.evaluateNode(prop.key, context);
            if (typeof key !== 'string') {
                throw new RuntimeError(`Object key must be string, got: ${typeof key}`, node.start, node);
            }
            const value = this.evaluateNode(prop.value, context);
            result[key] = value;
        }
        return result;
    }
    evaluateStringTemplate(node, context) {
        let result = '';
        for (const part of node.parts) {
            if (part.type === 'Literal') {
                result += part.value;
            }
            else {
                const value = this.evaluateNode(part, context);
                result += this.valueToString(value);
            }
        }
        return result;
    }
    evaluateConditional(node, context) {
        const condition = this.evaluateNode(node.condition, context);
        if (this.isTruthy(condition)) {
            return this.evaluateNode(node.thenExpression, context);
        }
        for (const elif of node.elifs) {
            const elifCondition = this.evaluateNode(elif.condition, context);
            if (this.isTruthy(elifCondition)) {
                return this.evaluateNode(elif.expression, context);
            }
        }
        return this.evaluateNode(node.elseExpression, context);
    }
    evaluateRange(node, context) {
        const iterable = this.evaluateNode(node.iterable, context);
        if (!Array.isArray(iterable)) {
            throw new RuntimeError(`Range requires array, got: ${typeof iterable}`, node.start, node);
        }
        const results = [];
        for (let i = 0; i < iterable.length; i++) {
            const item = iterable[i];
            // Create new context for the loop
            const loopContext = {
                variables: new Map(context.variables),
                functions: context.functions,
                parent: context
            };
            // Set loop variables
            loopContext.variables.set(node.variable, item);
            if (node.index) {
                loopContext.variables.set(node.index, i);
            }
            const result = this.evaluateNode(node.body, loopContext);
            results.push(result);
        }
        return results;
    }
    // Helper methods
    deepEqual(a, b) {
        if (a === b)
            return true;
        if (a === null || b === null)
            return a === b;
        if (a === undefined || b === undefined)
            return a === b;
        if (typeof a !== typeof b)
            return false;
        if (Array.isArray(a) && Array.isArray(b)) {
            if (a.length !== b.length)
                return false;
            return a.every((item, index) => this.deepEqual(item, b[index]));
        }
        if (typeof a === 'object' && typeof b === 'object') {
            const aObj = a;
            const bObj = b;
            const aKeys = Object.keys(aObj);
            const bKeys = Object.keys(bObj);
            if (aKeys.length !== bKeys.length)
                return false;
            return aKeys.every(key => this.deepEqual(aObj[key], bObj[key]));
        }
        return false;
    }
    compare(a, b, node) {
        if (typeof a === 'number' && typeof b === 'number') {
            return a - b;
        }
        if (typeof a === 'string' && typeof b === 'string') {
            return a.localeCompare(b);
        }
        throw new RuntimeError(`Cannot compare ${typeof a} and ${typeof b}`, node.start, node);
    }
    evaluateInOperator(left, right, node) {
        if (Array.isArray(right)) {
            return right.some(item => this.deepEqual(left, item));
        }
        if (typeof right === 'object' && right !== null) {
            if (typeof left === 'string') {
                return left in right;
            }
        }
        throw new RuntimeError(`'in' operator requires array or object on right side, got: ${typeof right}`, node.start, node);
    }
    isTruthy(value) {
        if (value === null || value === undefined)
            return false;
        if (typeof value === 'boolean')
            return value;
        if (typeof value === 'number')
            return value !== 0;
        if (typeof value === 'string')
            return value.length > 0;
        if (Array.isArray(value))
            return value.length > 0;
        if (typeof value === 'object')
            return Object.keys(value).length > 0;
        return true;
    }
    valueToString(value) {
        if (value === null)
            return 'null';
        if (value === undefined)
            return 'undefined';
        if (typeof value === 'string')
            return value;
        if (typeof value === 'number' || typeof value === 'boolean')
            return String(value);
        if (Array.isArray(value) || typeof value === 'object') {
            return JSON.stringify(value);
        }
        return String(value);
    }
}
// Utility functions
function evaluateKriti(node, variables = {}, customFunctions) {
    const interpreter = new KritiInterpreter(variables, customFunctions);
    return interpreter.evaluate(node);
}

function evaluate(input, variables = {}, customFunctions = new Map()) {
    const tokens = tokenizeKriti(input);
    const ast = parseKriti(tokens);
    return evaluateKriti(ast, variables, customFunctions);
}

export { evaluate, evaluateKriti as evaluateAST, parseKriti as parseTokens, tokenizeKriti as tokenize };
//# sourceMappingURL=index.esm.js.map