import json import logging import re from flask import Flask, request, jsonify from routes import app # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Global Symbol table and Output collector symbols = {} output = [] # Regular expressions for tokenizing TOKEN_REGEX = re.compile(r'\s*(?:(\()|(\))|(".*?")|([^()\s]+))') # Exception class for interpreter errors class InterpreterError(Exception): def __init__(self, message, line): self.message = message self.line = line # Helper functions for type checking and conversion def is_number(value): return isinstance(value, int) or isinstance(value, float) def convert_str(value): if isinstance(value, str): return value elif is_number(value): if isinstance(value, float): # Format to remove trailing zeros, keeping up to 4 decimal places return f"{value:.4f}".rstrip('0').rstrip('.') if '.' in f"{value:.4f}" else f"{value:.4f}" elif isinstance(value, int): return str(value) elif isinstance(value, bool): return "true" if value else "false" elif value is None: return "null" else: raise ValueError("Unsupported type for str conversion") # Function implementations def func_puts(args, line): if len(args) != 1: raise InterpreterError("puts expects exactly one argument", line) arg = args[0] if not isinstance(arg, str): raise InterpreterError("puts expects a string argument", line) logger.info(f"puts: Appending '{arg}' to output") output.append(arg) return None def func_set(args, line): if len(args) != 2: raise InterpreterError("set expects exactly two arguments", line) var_name, value = args if not isinstance(var_name, str): raise InterpreterError("set expects the first argument to be a string (variable name)", line) # Variable names must be alphabetic if not re.fullmatch(r'[A-Za-z]+', var_name): raise InterpreterError("Invalid variable name format", line) if var_name in symbols: raise InterpreterError("Cannot reassign to an existing constant", line) symbols[var_name] = value logger.info(f"set: '{var_name}' set to '{value}'") return None def func_concat(args, line): if len(args) != 2: raise InterpreterError("concat expects exactly two arguments", line) a, b = args if not isinstance(a, str) or not isinstance(b, str): raise InterpreterError("concat expects string arguments", line) result = a + b logger.info(f"concat: '{a}' + '{b}' = '{result}'") return result def func_substring(args, line): if len(args) != 3: raise InterpreterError("substring expects exactly three arguments", line) source, start, end = args if not isinstance(source, str): raise InterpreterError("substring expects the first argument to be a string", line) if not is_number(start) or not is_number(end): raise InterpreterError("substring expects start and end indices to be numbers", line) start = int(start) end = int(end) if start < 0 or end < 0 or start > len(source) or end > len(source) or start > end: raise InterpreterError("substring indices out of bounds", line) result = source[start:end] logger.info(f"substring: '{source}'[{start}:{end}] = '{result}'") return result def func_lowercase(args, line): if len(args) != 1: raise InterpreterError("lowercase expects exactly one argument", line) a = args[0] if not isinstance(a, str): raise InterpreterError("lowercase expects a string argument", line) result = a.lower() logger.info(f"lowercase: '{a}' -> '{result}'") return result def func_uppercase(args, line): if len(args) != 1: raise InterpreterError("uppercase expects exactly one argument", line) a = args[0] if not isinstance(a, str): raise InterpreterError("uppercase expects a string argument", line) result = a.upper() logger.info(f"uppercase: '{a}' -> '{result}'") return result def func_replace(args, line): if len(args) != 3: raise InterpreterError("replace expects exactly three arguments", line) source, target, replacement = args if not isinstance(source, str) or not isinstance(target, str) or not isinstance(replacement, str): raise InterpreterError("replace expects string arguments", line) result = source.replace(target, replacement) logger.info(f"replace: '{source}' with '{target}' replaced by '{replacement}' -> '{result}'") return result def func_add(args, line): if len(args) < 2: raise InterpreterError("add expects at least two arguments", line) total = 0 for arg in args: if not is_number(arg): raise InterpreterError("add expects numeric arguments", line) total += arg total = round(total, 4) if isinstance(total, float) else total logger.info(f"add: sum({args}) = {total}") return total def func_subtract(args, line): if len(args) != 2: raise InterpreterError("subtract expects exactly two arguments", line) a, b = args if not is_number(a) or not is_number(b): raise InterpreterError("subtract expects numeric arguments", line) result = a - b result = round(result, 4) if isinstance(result, float) else result logger.info(f"subtract: {a} - {b} = {result}") return result def func_multiply(args, line): if len(args) < 2: raise InterpreterError("multiply expects at least two arguments", line) result = 1 for arg in args: if not is_number(arg): raise InterpreterError("multiply expects numeric arguments", line) result *= arg result = round(result, 4) if isinstance(result, float) else result logger.info(f"multiply: product({args}) = {result}") return result def func_divide(args, line): if len(args) != 2: raise InterpreterError("divide expects exactly two arguments", line) dividend, divisor = args if not is_number(dividend) or not is_number(divisor): raise InterpreterError("divide expects numeric arguments", line) if divisor == 0: raise InterpreterError("Division by zero", line) if isinstance(dividend, int) and isinstance(divisor, int): result = dividend // divisor else: result = round(dividend / divisor, 4) logger.info(f"divide: {dividend} / {divisor} = {result}") return result def func_abs(args, line): if len(args) != 1: raise InterpreterError("abs expects exactly one argument", line) a = args[0] if not is_number(a): raise InterpreterError("abs expects a numeric argument", line) result = abs(a) logger.info(f"abs: abs({a}) = {result}") return result def func_max(args, line): if len(args) < 1: raise InterpreterError("max expects at least one argument", line) if not all(is_number(arg) for arg in args): raise InterpreterError("max expects numeric arguments", line) result = max(args) logger.info(f"max: max({args}) = {result}") return result def func_min(args, line): if len(args) < 1: raise InterpreterError("min expects at least one argument", line) if not all(is_number(arg) for arg in args): raise InterpreterError("min expects numeric arguments", line) result = min(args) logger.info(f"min: min({args}) = {result}") return result def func_gt(args, line): if len(args) != 2: raise InterpreterError("gt expects exactly two arguments", line) a, b = args if not is_number(a) or not is_number(b): raise InterpreterError("gt expects numeric arguments", line) result = a > b logger.info(f"gt: {a} > {b} = {result}") return result def func_lt(args, line): if len(args) != 2: raise InterpreterError("lt expects exactly two arguments", line) a, b = args if not is_number(a) or not is_number(b): raise InterpreterError("lt expects numeric arguments", line) result = a < b logger.info(f"lt: {a} < {b} = {result}") return result def func_equal(args, line): if len(args) != 2: raise InterpreterError("equal expects exactly two arguments", line) a, b = args result = a == b logger.info(f"equal: {a} == {b} = {result}") return result def func_not_equal(args, line): if len(args) != 2: raise InterpreterError("not_equal expects exactly two arguments", line) a, b = args result = a != b logger.info(f"not_equal: {a} != {b} = {result}") return result def func_str(args, line): if len(args) != 1: raise InterpreterError("str expects exactly one argument", line) a = args[0] try: result = convert_str(a) logger.info(f"str: {a} -> '{result}'") return result except ValueError: raise InterpreterError("str cannot convert the given type", line) # Mapping of function names to their implementations FUNCTIONS = { "puts": func_puts, "set": func_set, "concat": func_concat, "substring": func_substring, "lowercase": func_lowercase, "uppercase": func_uppercase, "replace": func_replace, "add": func_add, "subtract": func_subtract, "multiply": func_multiply, "divide": func_divide, "abs": func_abs, "max": func_max, "min": func_min, "gt": func_gt, "lt": func_lt, "equal": func_equal, "not_equal": func_not_equal, "str": func_str, } # Parser and evaluator def parse_expression(expression, line): tokens = tokenize(expression) if not tokens: raise InterpreterError("Empty expression", line) return parse_tokens(tokens, line) def tokenize(expression): tokens = TOKEN_REGEX.findall(expression) tokens = [tok for group in tokens for tok in group if tok] logger.debug(f"Tokenized expression: {tokens}") return tokens def parse_tokens(tokens, line): if not tokens: raise InterpreterError("Unexpected end of expression", line) token = tokens.pop(0) if token == '(': lst = [] while tokens and tokens[0] != ')': lst.append(parse_tokens(tokens, line)) if not tokens: raise InterpreterError("Expected ')'", line) tokens.pop(0) # Remove ')' logger.debug(f"Parsed list: {lst}") return lst elif token == ')': raise InterpreterError("Unexpected ')'", line) else: return atom(token, line) def atom(token, line): if token.startswith('"') and token.endswith('"'): value = token[1:-1] logger.debug(f"Atom: String '{value}'") return value elif token == "true": logger.debug("Atom: Boolean True") return True elif token == "false": logger.debug("Atom: Boolean False") return False elif token == "null": logger.debug("Atom: Null") return None else: # Try to parse as integer or float try: if '.' in token: value = float(token) logger.debug(f"Atom: Float {value}") return value else: value = int(token) logger.debug(f"Atom: Integer {value}") return value except ValueError: # It's a variable name if token in symbols: value = symbols[token] logger.debug(f"Atom: Variable '{token}' with value '{value}'") return value else: logger.debug(f"Atom: Undefined variable or function '{token}'") return token # Could be a function name def evaluate(ast, line): if isinstance(ast, list): if not ast: raise InterpreterError("Empty expression", line) func_name = ast[0] if not isinstance(func_name, str): raise InterpreterError("Function name must be a string", line) if func_name not in FUNCTIONS: raise InterpreterError(f"Undefined function '{func_name}'", line) func = FUNCTIONS[func_name] args = [] for arg in ast[1:]: args.append(evaluate(arg, line)) logger.debug(f"Evaluating function '{func_name}' with arguments {args}") return func(args, line) else: logger.debug(f"Returning atom: {ast}") return ast @app.route('/lisp-parser', methods=['POST']) def lisp_parser(): global symbols, output symbols = {} output = [] data = request.get_json() expressions = data.get("expressions", []) logger.info(f"Received expressions: {expressions}") for idx, expr in enumerate(expressions): line = idx + 1 logger.info(f"Processing line {line}: {expr}") try: ast = parse_expression(expr, line) logger.debug(f"AST for line {line}: {ast}") result = evaluate(ast, line) logger.debug(f"Result for line {line}: {result}") # If the result is a boolean or number and not part of 'puts', it's ignored except InterpreterError as e: error_message = f"ERROR at line {e.line}" logger.error(error_message) output.append(error_message) break except Exception as e: error_message = f"ERROR at line {line}" logger.error(f"{error_message}: {str(e)}") output.append(error_message) break logger.info(f"Final output: {output}") return jsonify({"output": output})