kriti-lang
Version:
A TypeScript implementation of the Kriti templating language
1,241 lines (1,238 loc) • 61.4 kB
JavaScript
'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