diff options
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r-- | js/scripting-lang/lang.js | 1796 |
1 files changed, 1574 insertions, 222 deletions
diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index f91f842..3de7a0e 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,320 +1,1672 @@ // The goal here is less to make anything useful...or even something that works, but to learn what parts an interpreted languages needs to have to function. +// Initialize standard library functions +function initializeStandardLibrary(scope) { + // Map: Apply a function to each element + scope.map = function(f, x) { + // Handle function references by calling them if they're functions + if (typeof f === 'function') { + return f(x); + } else { + throw new Error('map: first argument must be a function'); + } + }; + + // Compose: Compose two functions (f ∘ g)(x) = f(g(x)) + scope.compose = function(f, g, x) { + if (typeof f === 'function' && typeof g === 'function') { + return f(g(x)); + } else { + throw new Error('compose: first two arguments must be functions'); + } + }; + + // Curry: Convert a function that takes multiple arguments into a series of functions + // Since our language already uses curried functions by default, this is mostly for explicit currying + scope.curry = function(f, x, y) { + if (typeof f === 'function') { + return f(x, y); + } else { + throw new Error('curry: first argument must be a function'); + } + }; + + // Apply: Apply a function to an argument (same as function call, but more explicit) + scope.apply = function(f, x) { + if (typeof f === 'function') { + return f(x); + } else { + throw new Error('apply: first argument must be a function'); + } + }; + + // Pipe: Compose functions in left-to-right order (opposite of compose) + // pipe f g x = g f x + scope.pipe = function(f, g, x) { + if (typeof f === 'function' && typeof g === 'function') { + return g(f(x)); + } else { + throw new Error('pipe: first two arguments must be functions'); + } + }; + + // Filter: Filter based on a predicate + // For now, we'll implement it as a higher-order function + scope.filter = function(p, x) { + if (typeof p === 'function') { + return p(x) ? x : 0; + } else { + throw new Error('filter: first argument must be a function'); + } + }; + + // Reduce: Reduce to a single value using a binary function + // For now, we'll implement it as a higher-order function + scope.reduce = function(f, init, x) { + if (typeof f === 'function') { + return f(init, x); + } else { + throw new Error('reduce: first argument must be a function'); + } + }; + + // Fold: Same as reduce, but more explicit about the folding direction + scope.fold = function(f, init, x) { + if (typeof f === 'function') { + return f(init, x); + } else { + throw new Error('fold: first argument must be a function'); + } + }; +} + // Define the types of tokens const TokenType = { NUMBER: 'NUMBER', PLUS: 'PLUS', + MINUS: 'MINUS', + MULTIPLY: 'MULTIPLY', + DIVIDE: 'DIVIDE', IDENTIFIER: 'IDENTIFIER', ASSIGNMENT: 'ASSIGNMENT', + ARROW: 'ARROW', + CASE: 'CASE', + OF: 'OF', + WILDCARD: 'WILDCARD', FUNCTION: 'FUNCTION', LEFT_PAREN: 'LEFT_PAREN', RIGHT_PAREN: 'RIGHT_PAREN', LEFT_BRACE: 'LEFT_BRACE', RIGHT_BRACE: 'RIGHT_BRACE', + LEFT_BRACKET: 'LEFT_BRACKET', + RIGHT_BRACKET: 'RIGHT_BRACKET', SEMICOLON: 'SEMICOLON', + COMMA: 'COMMA', + DOT: 'DOT', + STRING: 'STRING', + TRUE: 'TRUE', + FALSE: 'FALSE', + AND: 'AND', + OR: 'OR', + XOR: 'XOR', + NOT: 'NOT', + EQUALS: 'EQUALS', + LESS_THAN: 'LESS_THAN', + GREATER_THAN: 'GREATER_THAN', + LESS_EQUAL: 'LESS_EQUAL', + GREATER_EQUAL: 'GREATER_EQUAL', + NOT_EQUAL: 'NOT_EQUAL', + MODULO: 'MODULO', + POWER: 'POWER', + IO_IN: 'IO_IN', + IO_OUT: 'IO_OUT', + IO_ASSERT: 'IO_ASSERT', + FUNCTION_REF: 'FUNCTION_REF' }; -// Lexer +// Lexer - converts source code to tokens function lexer(input) { - const tokens = []; let current = 0; - + const tokens = []; + while (current < input.length) { let char = input[current]; - - if (/\d/.test(char)) { - let value = ''; - while (/\d/.test(char)) { - value += char; - char = input[++current]; - } - tokens.push({ - type: TokenType.NUMBER, - value - }); - continue; - } - - if (char === '+') { - tokens.push({ - type: TokenType.PLUS - }); + + // Skip whitespace + if (/\s/.test(char)) { current++; continue; } - - if (/[a-z]/i.test(char)) { - let value = ''; - while (/[a-z]/i.test(char)) { - value += char; - char = input[++current]; + + // Skip comments + if (char === '/' && input[current + 1] === '*') { + let commentDepth = 1; + current += 2; // Skip /* + + while (current < input.length && commentDepth > 0) { + if (input[current] === '/' && input[current + 1] === '*') { + commentDepth++; + current += 2; + } else if (input[current] === '*' && input[current + 1] === '/') { + commentDepth--; + current += 2; + } else { + current++; + } } - tokens.push({ - type: TokenType.IDENTIFIER, - value - }); - continue; - } - - if (char === ':') { - tokens.push({ - type: TokenType.ASSIGNMENT - }); - current++; - continue; - } - - if (char === '=') { - tokens.push({ - type: TokenType.EQUAL - }); - current++; - continue; - } - - if (input.slice(current, current + 2) === 'if') { - tokens.push({ - type: TokenType.IF - }); - current += 2; - continue; - } - - if (input.slice(current, current + 4) === 'else') { - tokens.push({ - type: TokenType.ELSE - }); - current += 4; continue; } - - if (char === '(') { - tokens.push({ - type: TokenType.LEFT_PAREN - }); - current++; - continue; - } - - if (char === ')') { - tokens.push({ - type: TokenType.RIGHT_PAREN - }); - current++; + + // Numbers + if (/[0-9]/.test(char)) { + let value = ''; + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; + current++; + } + + // Check for decimal point + if (current < input.length && input[current] === '.') { + value += input[current]; + current++; + + // Parse decimal part + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; + current++; + } + + tokens.push({ + type: TokenType.NUMBER, + value: parseFloat(value) + }); + } else { + tokens.push({ + type: TokenType.NUMBER, + value: parseInt(value) + }); + } continue; } - - if (char === '{') { - tokens.push({ - type: TokenType.LEFT_BRACE - }); - current++; + + // Strings + if (char === '"') { + let value = ''; + current++; // Skip opening quote + + while (current < input.length && input[current] !== '"') { + value += input[current]; + current++; + } + + if (current < input.length) { + current++; // Skip closing quote + tokens.push({ + type: TokenType.STRING, + value: value + }); + } else { + throw new Error('Unterminated string'); + } continue; } - - if (char === '}') { - tokens.push({ - type: TokenType.RIGHT_BRACE - }); - current++; + + // Identifiers and keywords + if (/[a-zA-Z_]/.test(char)) { + let value = ''; + while (current < input.length && /[a-zA-Z0-9_]/.test(input[current])) { + value += input[current]; + current++; + } + + // Check for keywords + switch (value) { + case 'case': + tokens.push({ type: TokenType.CASE }); + break; + case 'of': + tokens.push({ type: TokenType.OF }); + break; + case 'function': + tokens.push({ type: TokenType.FUNCTION }); + break; + case 'true': + tokens.push({ type: TokenType.TRUE }); + break; + case 'false': + tokens.push({ type: TokenType.FALSE }); + break; + case 'and': + tokens.push({ type: TokenType.AND }); + break; + case 'or': + tokens.push({ type: TokenType.OR }); + break; + case 'xor': + tokens.push({ type: TokenType.XOR }); + break; + case 'not': + tokens.push({ type: TokenType.NOT }); + break; + case '_': + tokens.push({ type: TokenType.WILDCARD }); + break; + default: + tokens.push({ + type: TokenType.IDENTIFIER, + value: value + }); + } continue; } - - if (input.slice(current, current + 8) === 'function') { - tokens.push({ - type: TokenType.FUNCTION - }); - current += 8; - continue; + + // Two-character operators + if (current + 1 < input.length) { + const twoChar = char + input[current + 1]; + switch (twoChar) { + case '->': + tokens.push({ type: TokenType.ARROW }); + current += 2; + continue; + case '==': + tokens.push({ type: TokenType.EQUALS }); + current += 2; + continue; + case '!=': + tokens.push({ type: TokenType.NOT_EQUAL }); + current += 2; + continue; + case '<=': + tokens.push({ type: TokenType.LESS_EQUAL }); + current += 2; + continue; + case '>=': + tokens.push({ type: TokenType.GREATER_EQUAL }); + current += 2; + continue; + case '..': + // Check for IO operations + if (current + 2 < input.length) { + const ioChar = input[current + 2]; + switch (ioChar) { + case 'i': + if (current + 3 < input.length && input[current + 3] === 'n') { + tokens.push({ type: TokenType.IO_IN }); + current += 4; + continue; + } + break; + case 'o': + if (current + 3 < input.length && input[current + 3] === 'u') { + if (current + 4 < input.length && input[current + 4] === 't') { + tokens.push({ type: TokenType.IO_OUT }); + current += 5; + continue; + } + } + break; + case 'a': + if (current + 3 < input.length && input[current + 3] === 's') { + if (current + 4 < input.length && input[current + 4] === 's') { + if (current + 5 < input.length && input[current + 5] === 'e') { + if (current + 6 < input.length && input[current + 6] === 'r') { + if (current + 7 < input.length && input[current + 7] === 't') { + tokens.push({ type: TokenType.IO_ASSERT }); + current += 8; + continue; + } + } + } + } + } + break; + } + } + // If we get here, it's not a complete IO operation, so skip the '..' + current += 2; + continue; + } } - - if (char === ';') { - tokens.push({ type: TokenType.SEMICOLON }); - current++; - continue; + + // Single character operators + switch (char) { + case '+': + tokens.push({ type: TokenType.PLUS }); + break; + case '-': + tokens.push({ type: TokenType.MINUS }); + break; + case '*': + tokens.push({ type: TokenType.MULTIPLY }); + break; + case '/': + tokens.push({ type: TokenType.DIVIDE }); + break; + case '%': + tokens.push({ type: TokenType.MODULO }); + break; + case '^': + tokens.push({ type: TokenType.POWER }); + break; + case ':': + tokens.push({ type: TokenType.ASSIGNMENT }); + break; + case '(': + tokens.push({ type: TokenType.LEFT_PAREN }); + break; + case ')': + tokens.push({ type: TokenType.RIGHT_PAREN }); + break; + case '{': + tokens.push({ type: TokenType.LEFT_BRACE }); + break; + case '}': + tokens.push({ type: TokenType.RIGHT_BRACE }); + break; + case '[': + tokens.push({ type: TokenType.LEFT_BRACKET }); + break; + case ']': + tokens.push({ type: TokenType.RIGHT_BRACKET }); + break; + case ';': + tokens.push({ type: TokenType.SEMICOLON }); + break; + case ',': + tokens.push({ type: TokenType.COMMA }); + break; + case '.': + tokens.push({ type: TokenType.DOT }); + break; + case '@': + tokens.push({ type: TokenType.FUNCTION_REF }); + break; + case '_': + tokens.push({ type: TokenType.WILDCARD }); + break; + case '=': + tokens.push({ type: TokenType.EQUALS }); + break; + case '<': + tokens.push({ type: TokenType.LESS_THAN }); + break; + case '>': + tokens.push({ type: TokenType.GREATER_THAN }); + break; + default: + throw new Error(`Unexpected character: ${char}`); } - + current++; } - + return tokens; } -// Parser +// Parser - converts tokens to AST function parser(tokens) { let current = 0; - + function walk() { - if (current >= tokens.length) { - return null; // Return null when there are no more tokens + function parseChainedDotAccess(tableExpr) { + let result = tableExpr; + + while (current < tokens.length && tokens[current].type === TokenType.DOT) { + current++; // Skip the dot + + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const key = { + type: 'Identifier', + value: tokens[current].value + }; + current++; + + result = { + type: 'TableAccess', + table: result, + key: key + }; + } else { + throw new Error('Expected identifier after dot'); + } + } + + return result; } - - let token = tokens[current]; - - if (token.type === TokenType.NUMBER) { - current++; - return { - type: 'NumberLiteral', - value: token.value, - }; + + function parseChainedTableAccess(tableExpr) { + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + current++; // Skip '[' + const keyExpr = walk(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACKET) { + current++; // Skip ']' + + const access = { + type: 'TableAccess', + table: tableExpr, + key: keyExpr + }; + + // Check for chained access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + return parseChainedDotAccess(access); + } + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(access); + } + + return access; + } else { + throw new Error('Expected closing bracket'); + } + } + + // Check for dot access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + const result = parseChainedDotAccess(tableExpr); + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(result); + } + + return result; + } + + return tableExpr; } - - if (token.type === TokenType.PLUS) { - current++; - return { - type: 'PlusExpression', - left: walk(), - right: walk(), - }; + + function detectAmbiguousFunctionCalls() { + // This is a placeholder for future ambiguous function call detection + // For now, we'll assume the parser handles function calls correctly } - - if (token.type === TokenType.IDENTIFIER) { - current++; + + function parseFunctionCall(functionName) { + const args = []; + + // Parse arguments until we hit a semicolon or other terminator + while (current < tokens.length && + tokens[current].type !== TokenType.SEMICOLON && + tokens[current].type !== TokenType.RIGHT_PAREN && + tokens[current].type !== TokenType.RIGHT_BRACE && + tokens[current].type !== TokenType.COMMA) { + args.push(parseExpression()); + } + return { - type: 'Identifier', - value: token.value, + type: 'FunctionCall', + name: functionName, + args: args }; } - - if (token.type === TokenType.ASSIGNMENT) { - current++; - return { - type: 'AssignmentExpression', - name: tokens[current - 2].value, - value: walk(), - }; + + function parseExpression() { + let left = parseTerm(); + + while (current < tokens.length && + (tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.EQUALS || + tokens[current].type === TokenType.NOT_EQUAL || + tokens[current].type === TokenType.LESS_THAN || + tokens[current].type === TokenType.GREATER_THAN || + tokens[current].type === TokenType.LESS_EQUAL || + tokens[current].type === TokenType.GREATER_EQUAL || + tokens[current].type === TokenType.AND || + tokens[current].type === TokenType.OR || + tokens[current].type === TokenType.XOR)) { + + const operator = tokens[current].type; + current++; + const right = parseTerm(); + + switch (operator) { + case TokenType.PLUS: + left = { type: 'PlusExpression', left, right }; + break; + case TokenType.MINUS: + left = { type: 'MinusExpression', left, right }; + break; + case TokenType.EQUALS: + left = { type: 'EqualsExpression', left, right }; + break; + case TokenType.NOT_EQUAL: + left = { type: 'NotEqualExpression', left, right }; + break; + case TokenType.LESS_THAN: + left = { type: 'LessThanExpression', left, right }; + break; + case TokenType.GREATER_THAN: + left = { type: 'GreaterThanExpression', left, right }; + break; + case TokenType.LESS_EQUAL: + left = { type: 'LessEqualExpression', left, right }; + break; + case TokenType.GREATER_EQUAL: + left = { type: 'GreaterEqualExpression', left, right }; + break; + case TokenType.AND: + left = { type: 'AndExpression', left, right }; + break; + case TokenType.OR: + left = { type: 'OrExpression', left, right }; + break; + case TokenType.XOR: + left = { type: 'XorExpression', left, right }; + break; + } + } + + return left; } - - if (token.type === TokenType.IF) { - current++; - let node = { - type: 'IfExpression', - test: walk(), - consequent: walk(), - alternate: tokens[current].type === TokenType.ELSE ? (current++, walk()) : null, - }; - return node; + + function parseTerm() { + let left = parseFactor(); + + while (current < tokens.length && + (tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO)) { + + const operator = tokens[current].type; + current++; + const right = parseFactor(); + + switch (operator) { + case TokenType.MULTIPLY: + left = { type: 'MultiplyExpression', left, right }; + break; + case TokenType.DIVIDE: + left = { type: 'DivideExpression', left, right }; + break; + case TokenType.MODULO: + left = { type: 'ModuloExpression', left, right }; + break; + } + } + + return left; } + + function parseFactor() { + let left = parsePrimary(); + + while (current < tokens.length && tokens[current].type === TokenType.POWER) { + current++; + const right = parsePrimary(); + left = { type: 'PowerExpression', left, right }; + } + + return left; + } + + function parsePrimary() { + const token = tokens[current]; + + if (token.type === TokenType.NOT) { + current++; + const operand = parsePrimary(); + return { type: 'NotExpression', operand }; + } + + if (token.type === TokenType.NUMBER) { + current++; + return { + type: 'NumberLiteral', + value: token.value + }; + } + + if (token.type === TokenType.STRING) { + current++; + return { + type: 'StringLiteral', + value: token.value + }; + } + + if (token.type === TokenType.TRUE) { + current++; + return { + type: 'BooleanLiteral', + value: true + }; + } + + if (token.type === TokenType.FALSE) { + current++; + return { + type: 'BooleanLiteral', + value: false + }; + } + + if (token.type === TokenType.LEFT_PAREN) { + current++; // Skip '(' + const parenthesizedExpr = parseExpression(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_PAREN) { + current++; // Skip ')' + return parenthesizedExpr; + } else { + throw new Error('Expected closing parenthesis'); + } + } + + if (token.type === TokenType.IDENTIFIER) { + const identifier = { + type: 'Identifier', + value: token.value + }; + current++; + + // Check if this is an assignment + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + + // Check if this is a function definition + let isFunction = false; + let params = []; + + // Look ahead to see if this is a function definition + let lookAhead = current; + while (lookAhead < tokens.length && + tokens[lookAhead].type !== TokenType.ARROW && + tokens[lookAhead].type !== TokenType.SEMICOLON) { + if (tokens[lookAhead].type === TokenType.IDENTIFIER) { + params.push(tokens[lookAhead].value); + } + lookAhead++; + } + + if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.ARROW) { + isFunction = true; + } + + if (isFunction) { + // Clear params array and parse function parameters + params = []; + while (current < tokens.length && tokens[current].type !== TokenType.ARROW) { + if (tokens[current].type === TokenType.IDENTIFIER) { + params.push(tokens[current].value); + } + current++; + } + + current++; // Skip '->' + + // Parse the function body (which could be a case expression or other expression) + const functionBody = parseExpression(); + + return { + type: 'AssignmentExpression', + name: identifier.value, + value: { + type: 'FunctionDeclaration', + name: null, // Anonymous function + params, + body: functionBody, + } + }; + } else { + // Regular assignment + const value = parseExpression(); + return { + type: 'AssignmentExpression', + name: identifier.value, + value: value + }; + } + } + + // Check if this is table access + if (current < tokens.length && + (tokens[current].type === TokenType.LEFT_BRACKET || + tokens[current].type === TokenType.DOT)) { + return parseChainedTableAccess(identifier); + } + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(identifier); + } + + return identifier; + } + + if (token.type === TokenType.FUNCTION_REF) { + current++; // Skip '@' + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const funcName = tokens[current].value; + current++; + return { + type: 'FunctionReference', + name: funcName + }; + } else { + throw new Error('Expected function name after @'); + } + } - if (token.type === TokenType.FUNCTION) { - current++; - let node = { - type: 'FunctionDeclaration', - name: tokens[current++].value, - params: [], - body: [], - }; - while (tokens[current].type !== TokenType.RIGHT_PAREN) { - node.params.push(tokens[current++].value); + if (token.type === TokenType.WILDCARD) { + current++; // Skip '_' + return { type: 'WildcardPattern' }; } - current++; // Skip right paren - while (tokens[current].type !== TokenType.RIGHT_BRACE) { - node.body.push(walk()); + + if (token.type === TokenType.CASE) { + current++; // Skip 'case' + + // Parse the value being matched + const value = parseExpression(); + + // Expect 'of' + if (tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); + } + current++; // Skip 'of' + + const cases = []; + + // Parse cases until we hit a semicolon or end + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + const pattern = parseExpression(); + + // Expect ':' after pattern + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + } else { + throw new Error('Expected ":" after pattern in case expression'); + } + + const result = parseExpression(); + cases.push({ + pattern: [pattern], + result: [result] + }); + } + + return { + type: 'CaseExpression', + value: [value], + cases, + }; } - current++; // Skip right brace - return node; - } + - if (token.type === TokenType.IDENTIFIER && tokens[current + 1].type === TokenType.LEFT_PAREN) { - current++; - let node = { - type: 'FunctionCall', - name: token.value, - args: [], - }; - current++; // Skip left paren - while (tokens[current].type !== TokenType.RIGHT_PAREN) { - node.args.push(walk()); + + // If we get here, it's an operator token that should be handled by parseExpression + // But we need to handle it here to avoid circular dependency + if (token.type === TokenType.LEFT_BRACE) { + current++; // Skip '{' + const entries = []; + let arrayIndex = 1; + + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { + // Skip leading commas + if (tokens[current].type === TokenType.COMMA) { + current++; + continue; + } + + let key = null; + let value; + + // Check if this is a key-value pair or just a value + if (current + 1 < tokens.length && tokens[current + 1].type === TokenType.ASSIGNMENT) { + // This is a key-value pair: key: value + if (tokens[current].type === TokenType.IDENTIFIER) { + key = { + type: 'Identifier', + value: tokens[current].value + }; + current++; // Skip the key + } else if (tokens[current].type === TokenType.NUMBER) { + key = { + type: 'NumberLiteral', + value: tokens[current].value, + }; + current++; // Skip the key + } else if (tokens[current].type === TokenType.STRING) { + key = { + type: 'StringLiteral', + value: tokens[current].value, + }; + current++; // Skip the key + } else if (tokens[current].type === TokenType.TRUE) { + key = { + type: 'BooleanLiteral', + value: true, + }; + current++; // Skip the key + } else if (tokens[current].type === TokenType.FALSE) { + key = { + type: 'BooleanLiteral', + value: false, + }; + current++; // Skip the key + } else { + throw new Error('Invalid key type in table literal'); + } + + current++; // Skip ':' + value = parseExpression(); + } else { + // This is just a value (array-like entry) + value = parseExpression(); + } + + entries.push({ key, value }); + + // Skip trailing commas + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; + } + } + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACE) { + current++; // Skip '}' + return { + type: 'TableLiteral', + entries: entries + }; + } else { + throw new Error('Expected closing brace'); + } + } + + // If we get here, it's an operator token that should be handled by parseExpression + // But we need to handle it here to avoid circular dependency + if (token.type === TokenType.PLUS || + token.type === TokenType.MINUS || + token.type === TokenType.MULTIPLY || + token.type === TokenType.DIVIDE || + token.type === TokenType.MODULO || + token.type === TokenType.POWER || + token.type === TokenType.EQUALS || + token.type === TokenType.NOT_EQUAL || + token.type === TokenType.LESS_THAN || + token.type === TokenType.GREATER_THAN || + token.type === TokenType.LESS_EQUAL || + token.type === TokenType.GREATER_EQUAL || + token.type === TokenType.AND || + token.type === TokenType.OR || + token.type === TokenType.XOR) { + // Reset current to parse the expression properly + return parseExpression(); } - current++; // Skip right paren - return node; + + // If we get here, we have an unexpected token + throw new Error(`Unexpected token in parsePrimary: ${token.type}`); } - - if (token.type === TokenType.SEMICOLON) { + + // Check for IO operations before calling parsePrimary + if (tokens[current].type === TokenType.IO_IN) { + current++; + return { type: 'IOInExpression' }; + } else if (tokens[current].type === TokenType.IO_OUT) { current++; - return; + const outputValue = parseExpression(); + return { type: 'IOOutExpression', value: outputValue }; + } else if (tokens[current].type === TokenType.IO_ASSERT) { + current++; + const assertionExpr = parseExpression(); + return { type: 'IOAssertExpression', value: assertionExpr }; } - - throw new TypeError(token.type); + + // Simple wrapper that calls parsePrimary for all token types + return parsePrimary(); } - - let ast = { + + const ast = { type: 'Program', - body: [], + body: [] }; - + while (current < tokens.length) { const node = walk(); - if (node !== null) { + if (node) { ast.body.push(node); } + + // Skip semicolons + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } } - + return ast; } // Interpreter function interpreter(ast) { - let globalScope = {}; - + const globalScope = {}; + initializeStandardLibrary(globalScope); + function evalNode(node) { + if (!node) { + return undefined; + } switch (node.type) { case 'NumberLiteral': - return parseInt(node.value); + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; case 'PlusExpression': return evalNode(node.left) + evalNode(node.right); + case 'MinusExpression': + return evalNode(node.left) - evalNode(node.right); + case 'MultiplyExpression': + return evalNode(node.left) * evalNode(node.right); + case 'DivideExpression': + const divisor = evalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return evalNode(node.left) / evalNode(node.right); + case 'ModuloExpression': + return evalNode(node.left) % evalNode(node.right); + case 'PowerExpression': + return Math.pow(evalNode(node.left), evalNode(node.right)); + case 'EqualsExpression': + return evalNode(node.left) === evalNode(node.right); + case 'LessThanExpression': + return evalNode(node.left) < evalNode(node.right); + case 'GreaterThanExpression': + return evalNode(node.left) > evalNode(node.right); + case 'LessEqualExpression': + return evalNode(node.left) <= evalNode(node.right); + case 'GreaterEqualExpression': + return evalNode(node.left) >= evalNode(node.right); + case 'NotEqualExpression': + return evalNode(node.left) !== evalNode(node.right); + case 'AndExpression': + return evalNode(node.left) && evalNode(node.right); + case 'OrExpression': + return evalNode(node.left) || evalNode(node.right); + case 'XorExpression': + const leftVal = evalNode(node.left); + const rightVal = evalNode(node.right); + return (leftVal && !rightVal) || (!leftVal && rightVal); + case 'NotExpression': + return !evalNode(node.operand); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = evalNode(entry.value); + arrayIndex++; + } else { + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = evalNode(entry.key); + } + const value = evalNode(entry.value); + table[key] = value; + } + } + + return table; + case 'TableAccess': + const tableValue = evalNode(node.table); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = evalNode(node.key); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; case 'AssignmentExpression': - globalScope[node.name] = evalNode(node.value); + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + const value = evalNode(node.value); + globalScope[node.name] = value; return; case 'Identifier': - return globalScope[node.value]; - case 'IfExpression': - return evalNode(node.test) ? evalNode(node.consequent) : node.alternate ? evalNode(node.alternate) : undefined; + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined) { + throw new Error(`Variable ${node.value} is not defined`); + } + return identifierValue; case 'FunctionDeclaration': - globalScope[node.name] = function() { + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + return function(...args) { let localScope = Object.create(globalScope); for (let i = 0; i < node.params.length; i++) { - localScope[node.params[i]] = arguments[i]; + localScope[node.params[i]] = args[i]; } - let lastResult; - for (let bodyNode of node.body) { - lastResult = evalNode(bodyNode); - } - return lastResult; + return localEvalNodeWithScope(node.body, localScope); }; - return; case 'FunctionCall': - if (globalScope[node.name] instanceof Function) { + let funcToCall; // Renamed from 'func' to avoid redeclaration + if (typeof node.name === 'string') { + // Regular function call with string name + funcToCall = globalScope[node.name]; + } else if (node.name.type === 'Identifier') { + // Function call with identifier + funcToCall = globalScope[node.name.value]; + } else if (node.name.type === 'TableAccess') { + // Function call from table access (e.g., math.add) + funcToCall = evalNode(node.name); + } else { + throw new Error('Invalid function name in function call'); + } + + if (funcToCall instanceof Function) { let args = node.args.map(evalNode); - return globalScope[node.name].apply(null, args); + return funcToCall(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'CaseExpression': + const values = node.value.map(evalNode); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(evalNode); + + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + if (patternValue === true) continue; + + if (value !== patternValue) { + matches = false; + break; + } + } + + if (matches) { + const results = caseItem.result.map(evalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const outputValue = evalNode(node.value); + console.log(outputValue); + return outputValue; + case 'IOAssertExpression': + const assertionValue = evalNode(node.value); + if (!assertionValue) { + throw new Error('Assertion failed'); + } + return assertionValue; + case 'FunctionReference': + const functionValue = globalScope[node.name]; + if (functionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); } - throw new Error(`Function ${node.name} is not defined`); + if (typeof functionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return functionValue; default: throw new Error(`Unknown node type: ${node.type}`); } } - return evalNode(ast.body[0]); + const localEvalNodeWithScope = (node, scope) => { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return localEvalNodeWithScope(node.left, scope) + localEvalNodeWithScope(node.right, scope); + case 'MinusExpression': + return localEvalNodeWithScope(node.left, scope) - localEvalNodeWithScope(node.right, scope); + case 'MultiplyExpression': + return localEvalNodeWithScope(node.left, scope) * localEvalNodeWithScope(node.right, scope); + case 'DivideExpression': + const divisor = localEvalNodeWithScope(node.right, scope); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return localEvalNodeWithScope(node.left, scope) / localEvalNodeWithScope(node.right, scope); + case 'ModuloExpression': + return localEvalNodeWithScope(node.left, scope) % localEvalNodeWithScope(node.right, scope); + case 'PowerExpression': + return Math.pow(localEvalNodeWithScope(node.left, scope), localEvalNodeWithScope(node.right, scope)); + case 'EqualsExpression': + return localEvalNodeWithScope(node.left, scope) === localEvalNodeWithScope(node.right, scope); + case 'LessThanExpression': + return localEvalNodeWithScope(node.left, scope) < localEvalNodeWithScope(node.right, scope); + case 'GreaterThanExpression': + return localEvalNodeWithScope(node.left, scope) > localEvalNodeWithScope(node.right, scope); + case 'LessEqualExpression': + return localEvalNodeWithScope(node.left, scope) <= localEvalNodeWithScope(node.right, scope); + case 'GreaterEqualExpression': + return localEvalNodeWithScope(node.left, scope) >= localEvalNodeWithScope(node.right, scope); + case 'NotEqualExpression': + return localEvalNodeWithScope(node.left, scope) !== localEvalNodeWithScope(node.right, scope); + case 'AndExpression': + return localEvalNodeWithScope(node.left, scope) && localEvalNodeWithScope(node.right, scope); + case 'OrExpression': + return localEvalNodeWithScope(node.left, scope) || localEvalNodeWithScope(node.right, scope); + case 'XorExpression': + const leftVal = localEvalNodeWithScope(node.left, scope); + const rightVal = localEvalNodeWithScope(node.right, scope); + return (leftVal && !rightVal) || (!leftVal && rightVal); + case 'NotExpression': + return !localEvalNodeWithScope(node.operand, scope); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNodeWithScope(entry.value, scope); + arrayIndex++; + } else { + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNodeWithScope(entry.key, scope); + } + const value = localEvalNodeWithScope(entry.value, scope); + table[key] = value; + } + } + + return table; + case 'TableAccess': + const tableValue = localEvalNodeWithScope(node.table, scope); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = localEvalNodeWithScope(node.key, scope); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + globalScope[node.name] = localEvalNodeWithScope(node.value, scope); + return; + case 'Identifier': + // First check local scope, then global scope + if (scope && scope.hasOwnProperty(node.value)) { + return scope[node.value]; + } + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined && node.value) { + return node.value; + } + return identifierValue; + case 'FunctionDeclaration': + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + return function(...args) { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + nestedScope[node.params[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, nestedScope); + }; + case 'FunctionCall': + let localFunc; + if (typeof node.name === 'string') { + // Regular function call with string name + localFunc = globalScope[node.name]; + } else if (node.name.type === 'Identifier') { + // Function call with identifier + localFunc = globalScope[node.name.value]; + } else if (node.name.type === 'TableAccess') { + // Function call from table access (e.g., math.add) + localFunc = localEvalNodeWithScope(node.name, scope); + } else { + throw new Error('Invalid function name in function call'); + } + + if (localFunc instanceof Function) { + let args = node.args.map(arg => localEvalNodeWithScope(arg, scope)); + return localFunc(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'CaseExpression': + const values = node.value.map(val => localEvalNodeWithScope(val, scope)); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope)); + + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + if (patternValue === true) continue; + + if (value !== patternValue) { + matches = false; + break; + } + } + + if (matches) { + const results = caseItem.result.map(res => localEvalNodeWithScope(res, scope)); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNodeWithScope(node.value, scope); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + const localAssertionValue = localEvalNodeWithScope(node.value, scope); + if (!localAssertionValue) { + throw new Error('Assertion failed'); + } + return localAssertionValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + default: + throw new Error(`Unknown node type: ${node.type}`); + } + }; + + const localEvalNode = (node) => { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return localEvalNode(node.left) + localEvalNode(node.right); + case 'MinusExpression': + return localEvalNode(node.left) - localEvalNode(node.right); + case 'MultiplyExpression': + return localEvalNode(node.left) * localEvalNode(node.right); + case 'DivideExpression': + const divisor = localEvalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return localEvalNode(node.left) / localEvalNode(node.right); + case 'ModuloExpression': + return localEvalNode(node.left) % localEvalNode(node.right); + case 'PowerExpression': + return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); + case 'EqualsExpression': + return localEvalNode(node.left) === localEvalNode(node.right); + case 'LessThanExpression': + return localEvalNode(node.left) < localEvalNode(node.right); + case 'GreaterThanExpression': + return localEvalNode(node.left) > localEvalNode(node.right); + case 'LessEqualExpression': + return localEvalNode(node.left) <= localEvalNode(node.right); + case 'GreaterEqualExpression': + return localEvalNode(node.left) >= localEvalNode(node.right); + case 'NotEqualExpression': + return localEvalNode(node.left) !== localEvalNode(node.right); + case 'AndExpression': + return localEvalNode(node.left) && localEvalNode(node.right); + case 'OrExpression': + return localEvalNode(node.left) || localEvalNode(node.right); + case 'XorExpression': + const leftVal = localEvalNode(node.left); + const rightVal = localEvalNode(node.right); + return (leftVal && !rightVal) || (!leftVal && rightVal); + case 'NotExpression': + return !localEvalNode(node.operand); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNode(entry.value); + arrayIndex++; + } else { + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNode(entry.key); + } + const value = localEvalNode(entry.value); + table[key] = value; + } + } + + return table; + case 'TableAccess': + const tableValue = localEvalNode(node.table); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = localEvalNode(node.key); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + globalScope[node.name] = localEvalNode(node.value); + return; + case 'Identifier': + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined && node.value) { + return node.value; + } + return identifierValue; + case 'FunctionDeclaration': + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + return function(...args) { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + nestedScope[node.params[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, nestedScope); + }; + case 'FunctionCall': + let localFunc; + if (typeof node.name === 'string') { + // Regular function call with string name + localFunc = globalScope[node.name]; + } else if (node.name.type === 'Identifier') { + // Function call with identifier + localFunc = globalScope[node.name.value]; + } else if (node.name.type === 'TableAccess') { + // Function call from table access (e.g., math.add) + localFunc = localEvalNode(node.name); + } else { + throw new Error('Invalid function name in function call'); + } + + if (localFunc instanceof Function) { + let args = node.args.map(localEvalNode); + return localFunc(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'CaseExpression': + const values = node.value.map(localEvalNode); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(localEvalNode); + + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + if (patternValue === true) continue; + + if (value !== patternValue) { + matches = false; + break; + } + } + + if (matches) { + const results = caseItem.result.map(localEvalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNode(node.value); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + const localAssertionValue = localEvalNode(node.value); + if (!localAssertionValue) { + throw new Error('Assertion failed'); + } + return localAssertionValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + default: + throw new Error(`Unknown node type: ${node.type}`); + } + }; + + let lastResult; + for (let node of ast.body) { + if (node) { + lastResult = evalNode(node); + } + } + + if (lastResult instanceof Promise) { + return lastResult.then(result => { + return result; + }); + } + + return lastResult; } -// Usage -// const tokens = lexer('2 + 2'); -// const ast = parser(tokens); -// const result = interpreter(ast); -// console.log(result); // 4 +// Debug logging function +function debugLog(message, data = null) { + if (process.env.DEBUG) { + console.log(`[DEBUG] ${message}`); + if (data) { + console.log(data); + } + } +} -// const tokens2 = lexer('x : 2 + 2'); -// const ast2 = parser(tokens2); -// const result2 = interpreter(ast2); -// console.log(result2); +// Debug error function +function debugError(message, error = null) { + if (process.env.DEBUG) { + console.error(`[DEBUG ERROR] ${message}`); + if (error) { + console.error(error); + } + } +} -const fs = require('fs'); +// Execute a file +function executeFile(filePath) { + try { + const fs = require('fs'); + const input = fs.readFileSync(filePath, 'utf8'); + + debugLog('Input:', input); + + const tokens = lexer(input); + debugLog('Tokens:', tokens); + + const ast = parser(tokens); + debugLog('AST:', JSON.stringify(ast, null, 2)); + + const result = interpreter(ast); + + if (result instanceof Promise) { + result.then(finalResult => { + if (finalResult !== undefined) { + console.log(finalResult); + } + }).catch(error => { + console.error(`Error executing file: ${error.message}`); + }); + } else { + if (result !== undefined) { + console.log(result); + } + } + } catch (error) { + console.error(`Error executing file: ${error.message}`); + } +} -// Read the input from a file -const input = fs.readFileSync('input.txt', 'utf-8'); +// Check command line arguments +const args = process.argv.slice(2); -// Usage -const tokens = lexer(input); -const ast = parser(tokens); -const result = interpreter(ast); -console.log(result); \ No newline at end of file +if (args.length === 0) { + console.error('Usage: node lang.js <file>'); + console.error(' Provide a file path to execute'); + process.exit(1); +} else if (args.length === 1) { + // Execute the file + const filePath = args[0]; + executeFile(filePath); +} else { + // Too many arguments + console.error('Usage: node lang.js <file>'); + console.error(' Provide exactly one file path to execute'); + process.exit(1); +} \ No newline at end of file |