We are currently working on new rules for what content should and shouldn't be allowed on this website, and are looking for feedback! See Esolang:2026 topicality proposal to view and give feedback on the current draft.

FlamePL/Python Interpreter

From Esolang
Jump to navigation Jump to search

Back to FlamePL

Due to the source code was too long, the author also posted it on PasteBin.

Do you really want to see the interpreter right here? Or can't you access PasteBin? No worries, just click 'Expand' on the right to see the code here.

############################################################################
# Python interpreter for FlamePL.                                          #
# Current Version: 1.2.                                                    #
# Made by PrySigneToFry (Zweide-Altunik on Pastebin).                      #
# Document: https://esolangs.org/wiki/FlamePL                              #
############################################################################

import re
import sys
import os
import math as pymath
from decimal import Decimal, getcontext

getcontext().prec = 80

# ----------------------------------------------------------------------
#  Data types
# ----------------------------------------------------------------------

class FlamePLType:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return self.name

INT_TYPE    = FlamePLType("int")
FLOAT_TYPE  = FlamePLType("float")
COMPLEX_TYPE= FlamePLType("complex")
LIST_TYPE   = FlamePLType("list")
PAIR_TYPE   = FlamePLType("pair")
DICT_TYPE   = FlamePLType("dict")
STR_TYPE    = FlamePLType("str")
NULL_TYPE   = FlamePLType("null")
FUNCTION_TYPE= FlamePLType("function")
CLASS_TYPE  = FlamePLType("class")
BOOL_TYPE   = FlamePLType("bool")
MODULE_TYPE = FlamePLType("module")

class FlamePLComplex:
    def __init__(self, real, imag):
        self.real = Decimal(real)
        self.imag = Decimal(imag)
    def __repr__(self):
        if self.imag < 0:
            return f"{self.real} - {abs(self.imag)}i"
        return f"{self.real} + {self.imag}i"
    def __add__(self, other):
        if isinstance(other, FlamePLComplex):
            return FlamePLComplex(self.real + other.real, self.imag + other.imag)
        return FlamePLComplex(self.real + Decimal(other), self.imag)
    def __sub__(self, other):
        if isinstance(other, FlamePLComplex):
            return FlamePLComplex(self.real - other.real, self.imag - other.imag)
        return FlamePLComplex(self.real - Decimal(other), self.imag)
    def __mul__(self, other):
        if isinstance(other, FlamePLComplex):
            r = self.real * other.real - self.imag * other.imag
            i = self.real * other.imag + self.imag * other.real
            return FlamePLComplex(r, i)
        return FlamePLComplex(self.real * Decimal(other), self.imag * Decimal(other))
    def __truediv__(self, other):
        if isinstance(other, FlamePLComplex):
            denom = other.real**2 + other.imag**2
            r = (self.real * other.real + self.imag * other.imag) / denom
            i = (self.imag * other.real - self.real * other.imag) / denom
            return FlamePLComplex(r, i)
        return FlamePLComplex(self.real / Decimal(other), self.imag / Decimal(other))
    def __eq__(self, other):
        if not isinstance(other, FlamePLComplex):
            return False
        return self.real == other.real and self.imag == other.imag
    def __mod__(self, other):
        if isinstance(other, FlamePLComplex):
            return FlamePLComplex(self.real % other.real, self.imag % other.imag)
        return FlamePLComplex(self.real % Decimal(other), self.imag % Decimal(other))

class FlamePLPair:
    def __init__(self, first, second):
        self.first = first
        self.second = second
    def __repr__(self):
        return f"({self.first}, {self.second})"

class FlamePLModule:
    def __init__(self, env, name):
        self.env = env
        self.name = name
    def __getattr__(self, name):
        if name == "env":
            return self.env
        try:
            return self.env.get(name)
        except NameError:
            raise AttributeError(f"module {self.name} has no attribute {name}")
    def __repr__(self):
        return f"<module {self.name}>"

# ----------------------------------------------------------------------
#  Environment
# ----------------------------------------------------------------------

class Environment:
    def __init__(self, outer=None):
        self.outer = outer
        self.data = {}

    def get(self, name):
        if name in self.data:
            return self.data[name]
        if self.outer:
            return self.outer.get(name)
        raise NameError(f"Undefined variable: {name}")

    def set(self, name, value):
        self.data[name] = value

    def define(self, name, value):
        self.data[name] = value

    def extend(self):
        return Environment(self)

# ----------------------------------------------------------------------
#  AST nodes
# ----------------------------------------------------------------------

class ASTNode:
    pass

class Number(ASTNode):
    def __init__(self, value):
        self.value = Decimal(value) if isinstance(value, (int, float, str)) else value

class ImagLiteral(ASTNode):
    def __init__(self, value):
        self.value = Decimal(value)

class String(ASTNode):
    def __init__(self, value):
        self.value = value

class BooleanLiteral(ASTNode):
    def __init__(self, value):
        self.value = value

class ListLiteral(ASTNode):
    def __init__(self, elements):
        self.elements = elements

class PairLiteral(ASTNode):
    def __init__(self, first, second):
        self.first = first
        self.second = second

class DictLiteral(ASTNode):
    def __init__(self, items):
        self.items = items

class NullLiteral(ASTNode):
    pass

class Var(ASTNode):
    def __init__(self, name):
        self.name = name

class BinaryOp(ASTNode):
    def __init__(self, op, left, right):
        self.op = op
        self.left = left
        self.right = right

class UnaryOp(ASTNode):
    def __init__(self, op, expr):
        self.op = op
        self.expr = expr

class Assign(ASTNode):
    def __init__(self, name, expr):
        self.name = name
        self.expr = expr

class If(ASTNode):
    def __init__(self, cond, then_block, elif_clauses, else_block):
        self.cond = cond
        self.then_block = then_block
        self.elif_clauses = elif_clauses
        self.else_block = else_block

class While(ASTNode):
    def __init__(self, cond, body):
        self.cond = cond
        self.body = body

class ForLoop(ASTNode):
    def __init__(self, var_name, iterable, body):
        self.var_name = var_name
        self.iterable = iterable
        self.body = body

class Return(ASTNode):
    def __init__(self, expr):
        self.expr = expr

class FunctionDef(ASTNode):
    def __init__(self, name, params, body):
        self.name = name
        self.params = params
        self.body = body

class LambdaDef(ASTNode):
    def __init__(self, params, body):
        self.params = params
        self.body = body

class ClassDef(ASTNode):
    def __init__(self, name, parent_name, methods):
        self.name = name
        self.parent_name = parent_name
        self.methods = methods

class Call(ASTNode):
    def __init__(self, func, args):
        self.func = func
        self.args = args

class MethodCall(ASTNode):
    def __init__(self, obj, method, args):
        self.obj = obj
        self.method = method
        self.args = args

class Index(ASTNode):
    def __init__(self, obj, index):
        self.obj = obj
        self.index = index

class Print(ASTNode):
    def __init__(self, expr):
        self.expr = expr

class Input(ASTNode):
    def __init__(self, prompt):
        self.prompt = prompt

class TypeOf(ASTNode):
    def __init__(self, expr):
        self.expr = expr

class Convert(ASTNode):
    def __init__(self, kind, expr):
        self.kind = kind
        self.expr = expr

class Import(ASTNode):
    def __init__(self, module_name, alias=None):
        self.module_name = module_name
        self.alias = alias

class FromImport(ASTNode):
    def __init__(self, module_name, imports):
        self.module_name = module_name
        self.imports = imports

# ----------------------------------------------------------------------
#  Callable objects
# ----------------------------------------------------------------------

class FlamePLFunction:
    def __init__(self, params, body, env, name=None):
        self.params = params
        self.body = body
        self.env = env
        self.name = name or "<lambda>"

    def call(self, args, evaluator):
        if len(args) != len(self.params):
            raise TypeError(f"{self.name} expects {len(self.params)} args, got {len(args)}")
        new_env = Environment(self.env)
        for p, a in zip(self.params, args):
            new_env.define(p, a)
        try:
            evaluator.evaluate_block(self.body, new_env)
        except ReturnException as e:
            return e.value
        return None

class FlamePLBoundMethod:
    def __init__(self, obj, method_fn):
        self.obj = obj
        self.method_fn = method_fn

    def call(self, args, evaluator):
        return self.method_fn.call([self.obj] + args, evaluator)

class FlamePLClass:
    def __init__(self, name, methods, parent=None):
        self.name = name
        self.methods = methods
        self.parent = parent

    def call(self, args, evaluator):
        instance = FlamePLInstance(self)
        init = self.get_method("init")
        if init:
            init.call([instance] + args, evaluator)
        return instance

    def get_method(self, name):
        if name in self.methods:
            return self.methods[name]
        if self.parent:
            return self.parent.get_method(name)
        return None

class FlamePLInstance:
    def __init__(self, klass):
        self.klass = klass
        self.fields = {}

    def get_method(self, name):
        method_fn = self.klass.get_method(name)
        if method_fn:
            return FlamePLBoundMethod(self, method_fn)
        return None

# ----------------------------------------------------------------------
#  Exception for return
# ----------------------------------------------------------------------

class ReturnException(Exception):
    def __init__(self, value):
        self.value = value

# ----------------------------------------------------------------------
#  Lexer
# ----------------------------------------------------------------------

class Token:
    def __init__(self, type_, value, line):
        self.type = type_
        self.value = value
        self.line = line
    def __repr__(self):
        return f"Token({self.type}, {repr(self.value)})"

class Lexer:
    def __init__(self, source):
        self.source = source
        self.pos = 0
        self.line = 1
        self.tokens = []

    def tokenize(self):
        while self.pos < len(self.source):
            ch = self.source[self.pos]
            if ch in ' \t\r':
                self.pos += 1
                continue
            if ch == '\n':
                self.line += 1
                self.pos += 1
                continue

            # Comments
            if ch == '#':
                self.pos += 1
                while self.pos < len(self.source) and self.source[self.pos] != '\n':
                    self.pos += 1
                continue

            # Multi‑line string
            if ch == '"' and self.pos + 2 < len(self.source) and self.source[self.pos:self.pos+3] == '"""':
                self.pos += 3
                start_line = self.line
                start_pos = self.pos
                while self.pos < len(self.source):
                    if self.source[self.pos:self.pos+3] == '"""':
                        self.pos += 3
                        content = self.source[start_pos:self.pos-3]
                        self.tokens.append(Token('STRING', content, start_line))
                        break
                    if self.source[self.pos] == '\n':
                        self.line += 1
                    self.pos += 1
                continue

            # Single‑line string
            if ch == '"':
                self.pos += 1
                start_pos = self.pos
                while self.pos < len(self.source) and self.source[self.pos] != '"':
                    if self.source[self.pos] == '\n':
                        self.line += 1
                    self.pos += 1
                if self.pos >= len(self.source):
                    raise SyntaxError("Unterminated string")
                content = self.source[start_pos:self.pos]
                self.pos += 1
                self.tokens.append(Token('STRING', content, self.line))
                continue

            # Assignment <-
            if ch == '<' and self.pos + 1 < len(self.source) and self.source[self.pos+1] == '-':
                self.tokens.append(Token('ASSIGN', '<-', self.line))
                self.pos += 2
                continue

            # Integer division //
            if ch == '/' and self.pos + 1 < len(self.source) and self.source[self.pos+1] == '/':
                self.tokens.append(Token('OP', '//', self.line))
                self.pos += 2
                continue

            # Multi‑character comparison operators: <=, >=, !=
            if ch == '<' and self.pos + 1 < len(self.source) and self.source[self.pos+1] == '=':
                self.tokens.append(Token('OP', '<=', self.line))
                self.pos += 2
                continue
            if ch == '>' and self.pos + 1 < len(self.source) and self.source[self.pos+1] == '=':
                self.tokens.append(Token('OP', '>=', self.line))
                self.pos += 2
                continue
            if ch == '!' and self.pos + 1 < len(self.source) and self.source[self.pos+1] == '=':
                self.tokens.append(Token('OP', '!=', self.line))
                self.pos += 2
                continue
            # Support Unicode ≠ as synonym for !=
            if ch == '≠':
                self.tokens.append(Token('OP', '!=', self.line))
                self.pos += 1
                continue

            # Exponent ^
            if ch == '^':
                self.tokens.append(Token('OP', '^', self.line))
                self.pos += 1
                continue

            # Imaginary literal
            imag_match = re.match(r'^(\d+)i', self.source[self.pos:])
            if imag_match:
                val = imag_match.group(1)
                self.tokens.append(Token('IMAG', val, self.line))
                self.pos += len(imag_match.group(0))
                continue

            # Number
            num_match = re.match(r'^(\d+\.\d+|\d+)', self.source[self.pos:])
            if num_match:
                val = num_match.group(1)
                self.tokens.append(Token('NUMBER', val, self.line))
                self.pos += len(num_match.group(0))
                continue

            # Single‑character operators and delimiters
            if ch in '+-*/=(){}[],:<>.':
                if ch == '=':
                    self.tokens.append(Token('OP', '=', self.line))
                elif ch == '<':
                    self.tokens.append(Token('OP', '<', self.line))
                elif ch == '>':
                    self.tokens.append(Token('OP', '>', self.line))
                else:
                    self.tokens.append(Token('OP', ch, self.line))
                self.pos += 1
                continue

            # Keywords and identifiers
            ident_match = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)', self.source[self.pos:])
            if ident_match:
                word = ident_match.group(1)
                keywords = {
                    'fn', 'class', 'if', 'then', 'else', 'elseif', 'end',
                    'while', 'do', 'return', 'null', 'lambda', 'and', 'or',
                    'not', 'true', 'false', 'for', 'in', 'import', 'from', 'as'
                }
                if word in keywords:
                    self.tokens.append(Token('KEYWORD', word, self.line))
                else:
                    self.tokens.append(Token('IDENTIFIER', word, self.line))
                self.pos += len(word)
                continue

            raise SyntaxError(f"Unexpected char '{ch}' at line {self.line}")

        self.tokens.append(Token('EOF', None, self.line))
        return self.tokens

# ----------------------------------------------------------------------
#  Parser
# ----------------------------------------------------------------------

class Parser:
    def __init__(self, tokens):
        self.tokens = tokens
        self.pos = 0

    def peek(self):
        return self.tokens[self.pos]

    def consume(self, expected_type=None, expected_value=None):
        tok = self.peek()
        if expected_type is not None and tok.type != expected_type:
            raise SyntaxError(f"Expected {expected_type}, got {tok.type} at line {tok.line}")
        if expected_value is not None and tok.value != expected_value:
            raise SyntaxError(f"Expected '{expected_value}', got '{tok.value}' at line {tok.line}")
        self.pos += 1
        return tok

    def match(self, type_, value=None):
        tok = self.peek()
        if tok.type == type_ and (value is None or tok.value == value):
            self.pos += 1
            return True
        return False

    def parse(self):
        statements = []
        while not self.match('EOF'):
            statements.append(self.parse_statement())
        return statements

    def parse_statement(self):
        tok = self.peek()
        if tok.type == 'KEYWORD':
            if tok.value == 'fn':
                return self.parse_function_def()
            elif tok.value == 'class':
                return self.parse_class_def()
            elif tok.value == 'if':
                return self.parse_if()
            elif tok.value == 'while':
                return self.parse_while()
            elif tok.value == 'for':
                return self.parse_for()
            elif tok.value == 'return':
                self.consume('KEYWORD', 'return')
                expr = self.parse_expression()
                return Return(expr)
            elif tok.value == 'print':
                self.consume('KEYWORD', 'print')
                self.consume('OP', '(')
                expr = self.parse_expression()
                self.consume('OP', ')')
                return Print(expr)
            elif tok.value == 'input':
                self.consume('KEYWORD', 'input')
                self.consume('OP', '(')
                prompt = self.parse_expression()
                self.consume('OP', ')')
                return Input(prompt)
            elif tok.value == 'import':
                return self.parse_import()
            elif tok.value == 'from':
                return self.parse_from_import()
        # If not a keyword, try expression (which may be assignment)
        left = self.parse_expression()
        if self.match('ASSIGN'):
            if not isinstance(left, Var):
                raise SyntaxError("Left side of assignment must be a variable")
            right = self.parse_expression()
            return Assign(left.name, right)
        return left

    def parse_import(self):
        self.consume('KEYWORD', 'import')
        tok = self.peek()
        if tok.type == 'STRING':
            name_node = self.consume('STRING')
            module_name = name_node.value
        elif tok.type == 'IDENTIFIER':
            name_node = self.consume('IDENTIFIER')
            module_name = name_node.value
        else:
            raise SyntaxError("Expected string or identifier after import")
        alias = None
        if self.match('KEYWORD', 'as'):
            alias_tok = self.consume('IDENTIFIER')
            alias = alias_tok.value
        return Import(module_name, alias)

    def parse_from_import(self):
        self.consume('KEYWORD', 'from')
        tok = self.peek()
        if tok.type == 'STRING':
            name_node = self.consume('STRING')
            module_name = name_node.value
        elif tok.type == 'IDENTIFIER':
            name_node = self.consume('IDENTIFIER')
            module_name = name_node.value
        else:
            raise SyntaxError("Expected string or identifier after from")
        self.consume('KEYWORD', 'import')
        imports = []
        while True:
            name_tok = self.consume('IDENTIFIER')
            orig = name_tok.value
            alias = None
            if self.match('KEYWORD', 'as'):
                alias_tok = self.consume('IDENTIFIER')
                alias = alias_tok.value
            imports.append((orig, alias))
            if not self.match('OP', ','):
                break
        return FromImport(module_name, imports)

    def parse_function_def(self):
        self.consume('KEYWORD', 'fn')
        name_tok = self.consume('IDENTIFIER')
        self.consume('OP', '(')
        params = []
        if not self.match('OP', ')'):
            while True:
                p = self.consume('IDENTIFIER')
                params.append(p.value)
                if self.match('OP', ','):
                    continue
                self.consume('OP', ')')
                break
        body = self.parse_block()
        self.consume('KEYWORD', 'end')
        return FunctionDef(name_tok.value, params, body)

    def parse_lambda(self):
        self.consume('KEYWORD', 'lambda')
        self.consume('OP', '(')
        params = []
        if not self.match('OP', ')'):
            while True:
                p = self.consume('IDENTIFIER')
                params.append(p.value)
                if self.match('OP', ','):
                    continue
                self.consume('OP', ')')
                break
        self.consume('OP', '->')
        body = self.parse_expression()
        return LambdaDef(params, [body])

    def parse_class_def(self):
        self.consume('KEYWORD', 'class')
        name_tok = self.consume('IDENTIFIER')
        parent_name = None
        if self.match('OP', ':'):
            parent_tok = self.consume('IDENTIFIER')
            parent_name = parent_tok.value
        methods = {}
        while True:
            tok = self.peek()
            if tok.type == 'KEYWORD' and tok.value == 'end':
                self.consume('KEYWORD', 'end')
                break
            if tok.type == 'KEYWORD' and tok.value == 'fn':
                fn_def = self.parse_function_def()
                methods[fn_def.name] = fn_def
            else:
                raise SyntaxError(f"Expected method definition inside class, got {tok}")
        return ClassDef(name_tok.value, parent_name, methods)

    def parse_block(self):
        stmts = []
        while True:
            tok = self.peek()
            if tok.type == 'KEYWORD' and tok.value in ('end', 'else', 'elseif'):
                break
            if tok.type == 'EOF':
                break
            stmts.append(self.parse_statement())
        return stmts

    def parse_if(self):
        self.consume('KEYWORD', 'if')
        cond = self.parse_expression()
        self.consume('KEYWORD', 'then')
        then_block = []
        while True:
            tok = self.peek()
            if tok.type == 'KEYWORD' and tok.value in ('else', 'elseif', 'end'):
                break
            then_block.append(self.parse_statement())

        elif_clauses = []
        while self.match('KEYWORD', 'elseif'):
            elif_cond = self.parse_expression()
            self.consume('KEYWORD', 'then')
            elif_block = []
            while True:
                tok = self.peek()
                if tok.type == 'KEYWORD' and tok.value in ('else', 'elseif', 'end'):
                    break
                elif_block.append(self.parse_statement())
            elif_clauses.append((elif_cond, elif_block))

        else_block = []
        if self.match('KEYWORD', 'else'):
            while True:
                tok = self.peek()
                if tok.type == 'KEYWORD' and tok.value == 'end':
                    break
                else_block.append(self.parse_statement())

        self.consume('KEYWORD', 'end')
        return If(cond, then_block, elif_clauses, else_block)

    def parse_while(self):
        self.consume('KEYWORD', 'while')
        cond = self.parse_expression()
        self.consume('KEYWORD', 'do')
        body = []
        while True:
            tok = self.peek()
            if tok.type == 'KEYWORD' and tok.value == 'end':
                break
            body.append(self.parse_statement())
        self.consume('KEYWORD', 'end')
        return While(cond, body)

    def parse_for(self):
        self.consume('KEYWORD', 'for')
        var_tok = self.consume('IDENTIFIER')
        self.consume('KEYWORD', 'in')
        iterable = self.parse_expression()
        self.consume('KEYWORD', 'do')
        body = []
        while True:
            tok = self.peek()
            if tok.type == 'KEYWORD' and tok.value == 'end':
                break
            body.append(self.parse_statement())
        self.consume('KEYWORD', 'end')
        return ForLoop(var_tok.value, iterable, body)

    def parse_expression(self):
        return self.parse_or()

    def parse_or(self):
        left = self.parse_and()
        while self.match('KEYWORD', 'or'):
            right = self.parse_and()
            left = BinaryOp('or', left, right)
        return left

    def parse_and(self):
        left = self.parse_not()
        while self.match('KEYWORD', 'and'):
            right = self.parse_not()
            left = BinaryOp('and', left, right)
        return left

    def parse_not(self):
        if self.match('KEYWORD', 'not'):
            expr = self.parse_not()
            return UnaryOp('not', expr)
        return self.parse_comparison()

    def parse_comparison(self):
        left = self.parse_additive()
        # Accept =, <, >, <=, >=, !=
        if self.match('OP', '=') or self.match('OP', '<') or self.match('OP', '>') or \
           self.match('OP', '<=') or self.match('OP', '>=') or self.match('OP', '!='):
            op = self.tokens[self.pos-1].value
            right = self.parse_additive()
            return BinaryOp(op, left, right)
        return left

    def parse_additive(self):
        left = self.parse_multiplicative()
        while self.match('OP', '+') or self.match('OP', '-'):
            op = self.tokens[self.pos-1].value
            right = self.parse_multiplicative()
            left = BinaryOp(op, left, right)
        return left

    def parse_multiplicative(self):
        left = self.parse_power()
        while self.match('OP', '*') or self.match('OP', '/') or self.match('OP', '//') or self.match('OP', '%'):
            op = self.tokens[self.pos-1].value
            right = self.parse_power()
            left = BinaryOp(op, left, right)
        return left

    def parse_power(self):
        left = self.parse_unary()
        if self.match('OP', '^'):
            right = self.parse_power()
            return BinaryOp('^', left, right)
        return left

    def parse_unary(self):
        if self.match('OP', '-') or self.match('OP', '+'):
            op = self.tokens[self.pos-1].value
            expr = self.parse_unary()
            return UnaryOp(op, expr)
        return self.parse_postfix()

    def parse_postfix(self):
        expr = self.parse_atom()
        while True:
            if self.match('OP', '['):
                idx = self.parse_expression()
                self.consume('OP', ']')
                expr = Index(expr, idx)
            elif self.match('OP', '('):
                args = []
                if not self.match('OP', ')'):
                    while True:
                        args.append(self.parse_expression())
                        if self.match('OP', ','):
                            continue
                        self.consume('OP', ')')
                        break
                expr = Call(expr, args)
            elif self.match('OP', '.'):
                method_tok = self.consume('IDENTIFIER')
                self.consume('OP', '(')
                args = []
                if not self.match('OP', ')'):
                    while True:
                        args.append(self.parse_expression())
                        if self.match('OP', ','):
                            continue
                        self.consume('OP', ')')
                        break
                expr = MethodCall(expr, method_tok.value, args)
            else:
                break
        return expr

    def parse_atom(self):
        tok = self.peek()
        if tok.type == 'NUMBER':
            self.consume('NUMBER')
            return Number(tok.value)
        if tok.type == 'IMAG':
            self.consume('IMAG')
            return ImagLiteral(tok.value)
        if tok.type == 'STRING':
            self.consume('STRING')
            return String(tok.value)
        if tok.type == 'KEYWORD' and tok.value in ('true', 'false'):
            self.consume('KEYWORD')
            return BooleanLiteral(tok.value == 'true')
        if tok.type == 'IDENTIFIER':
            self.consume('IDENTIFIER')
            if tok.value == 'null':
                return NullLiteral()
            if tok.value == 'to_int':
                self.consume('OP', '(')
                expr = self.parse_expression()
                self.consume('OP', ')')
                return Convert('int', expr)
            if tok.value == 'to_float':
                self.consume('OP', '(')
                expr = self.parse_expression()
                self.consume('OP', ')')
                return Convert('float', expr)
            if tok.value == 'to_str':
                self.consume('OP', '(')
                expr = self.parse_expression()
                self.consume('OP', ')')
                return Convert('str', expr)
            if tok.value == 'type_of':
                self.consume('OP', '(')
                expr = self.parse_expression()
                self.consume('OP', ')')
                return TypeOf(expr)
            return Var(tok.value)
        if tok.type == 'OP' and tok.value == '(':
            self.consume('OP', '(')
            expr = self.parse_expression()
            if self.match('OP', ','):
                second = self.parse_expression()
                self.consume('OP', ')')
                return PairLiteral(expr, second)
            self.consume('OP', ')')
            return expr
        if tok.type == 'OP' and tok.value == '[':
            self.consume('OP', '[')
            elements = []
            if not self.match('OP', ']'):
                while True:
                    elements.append(self.parse_expression())
                    if self.match('OP', ','):
                        continue
                    self.consume('OP', ']')
                    break
            return ListLiteral(elements)
        if tok.type == 'OP' and tok.value == '{':
            self.consume('OP', '{')
            items = []
            if not self.match('OP', '}'):
                while True:
                    key = self.parse_expression()
                    self.consume('OP', ':')
                    value = self.parse_expression()
                    items.append((key, value))
                    if self.match('OP', ','):
                        continue
                    self.consume('OP', '}')
                    break
            return DictLiteral(items)
        if tok.type == 'KEYWORD' and tok.value == 'lambda':
            return self.parse_lambda()
        raise SyntaxError(f"Unexpected token {tok}")

# ----------------------------------------------------------------------
#  Evaluator
# ----------------------------------------------------------------------

class Evaluator:
    def __init__(self, global_env):
        self.global_env = global_env
        self.loaded_modules = {}
        self.loading = set()
        self.current_file_dir = "."

    def resolve_module_path(self, name, base_dir):
        if isinstance(name, str) and name.endswith('.flpl'):
            if os.path.isabs(name):
                return name
            return os.path.normpath(os.path.join(base_dir, name))
        return name

    def load_builtin_module(self, name):
        if name == "math":
            env = Environment(self.global_env)
            def sqrt(x):
                if isinstance(x, FlamePLComplex):
                    raise TypeError("sqrt not supported for complex")
                return Decimal(pymath.sqrt(float(x)))
            def sin(x):
                return Decimal(pymath.sin(float(x)))
            def cos(x):
                return Decimal(pymath.cos(float(x)))
            env.define("sqrt", sqrt)
            env.define("sin", sin)
            env.define("cos", cos)
            env.define("pi", Decimal(pymath.pi))
            return env
        elif name == "io":
            env = Environment(self.global_env)
            return env
        else:
            raise ImportError(f"Unknown built-in module: {name}")

    def load_module(self, name, base_dir):
        path = self.resolve_module_path(name, base_dir)
        if path in self.loaded_modules:
            return self.loaded_modules[path]
        if path in self.loading:
            raise RuntimeError(f"Circular import detected: {name}")
        self.loading.add(path)

        if not isinstance(path, str) or '.' not in path or os.path.sep not in path:
            try:
                mod_env = self.load_builtin_module(path)
                self.loaded_modules[path] = mod_env
                self.loading.remove(path)
                return mod_env
            except ImportError:
                pass

        if not os.path.exists(path):
            if not path.endswith('.flpl'):
                path2 = path + '.flpl'
                if os.path.exists(path2):
                    path = path2
                else:
                    raise ImportError(f"Module not found: {name} (resolved as {path})")
            else:
                raise ImportError(f"Module not found: {name} (resolved as {path})")

        try:
            with open(path, 'r') as f:
                source = f.read()
        except Exception as e:
            raise ImportError(f"Could not read module {name}: {e}")

        mod_env = Environment(self.global_env)
        lexer = Lexer(source)
        tokens = lexer.tokenize()
        parser = Parser(tokens)
        ast = parser.parse()
        for stmt in ast:
            self.evaluate(stmt, mod_env)

        self.loaded_modules[path] = mod_env
        self.loading.remove(path)
        return mod_env

    def evaluate(self, node, env):
        if isinstance(node, Number):
            return node.value
        if isinstance(node, ImagLiteral):
            return FlamePLComplex(0, node.value)
        if isinstance(node, String):
            return node.value
        if isinstance(node, BooleanLiteral):
            return node.value
        if isinstance(node, NullLiteral):
            return None
        if isinstance(node, Var):
            return env.get(node.name)
        if isinstance(node, Assign):
            val = self.evaluate(node.expr, env)
            env.set(node.name, val)
            return val
        if isinstance(node, BinaryOp):
            left = self.evaluate(node.left, env)
            right = self.evaluate(node.right, env)
            op = node.op
            if op == 'and':
                return bool(left and right)
            if op == 'or':
                return bool(left or right)
            if op == '^':
                return left ** right
            if op == '//':
                return left // right
            if op == '%':
                return left % right
            if op == '+':
                return left + right
            if op == '-':
                return left - right
            if op == '*':
                return left * right
            if op == '/':
                return left / right
            if op == '=':
                return left == right
            if op == '<':
                return left < right
            if op == '>':
                return left > right
            if op == '<=':
                return left <= right
            if op == '>=':
                return left >= right
            if op == '!=':
                return left != right
            raise RuntimeError(f"Unknown binary op: {op}")
        if isinstance(node, UnaryOp):
            val = self.evaluate(node.expr, env)
            if node.op == '-':
                return -val
            if node.op == '+':
                return val
            if node.op == 'not':
                return not bool(val)
            raise RuntimeError(f"Unknown unary op: {node.op}")
        if isinstance(node, If):
            if self.evaluate(node.cond, env):
                return self.evaluate_block(node.then_block, env)
            for elif_cond, elif_block in node.elif_clauses:
                if self.evaluate(elif_cond, env):
                    return self.evaluate_block(elif_block, env)
            return self.evaluate_block(node.else_block, env)
        if isinstance(node, While):
            while self.evaluate(node.cond, env):
                self.evaluate_block(node.body, env)
            return None
        if isinstance(node, ForLoop):
            iterable_val = self.evaluate(node.iterable, env)
            if isinstance(iterable_val, list):
                items = iterable_val
            elif isinstance(iterable_val, dict):
                items = list(iterable_val.keys())
            elif isinstance(iterable_val, str):
                items = list(iterable_val)
            else:
                raise TypeError(f"Cannot iterate over {type(iterable_val)}")
            for item in items:
                env.set(node.var_name, item)
                self.evaluate_block(node.body, env)
            return None
        if isinstance(node, Return):
            raise ReturnException(self.evaluate(node.expr, env))
        if isinstance(node, FunctionDef):
            func = FlamePLFunction(node.params, node.body, env, node.name)
            env.set(node.name, func)
            return func
        if isinstance(node, LambdaDef):
            return FlamePLFunction(node.params, node.body, env)
        if isinstance(node, ClassDef):
            parent = None
            if node.parent_name:
                parent = env.get(node.parent_name)
                if not isinstance(parent, FlamePLClass):
                    raise TypeError(f"Parent {node.parent_name} is not a class")
            methods = {}
            for mname, fn_def in node.methods.items():
                methods[mname] = FlamePLFunction(fn_def.params, fn_def.body, env, f"{node.name}.{mname}")
            klass = FlamePLClass(node.name, methods, parent)
            env.set(node.name, klass)
            return klass
        if isinstance(node, Call):
            func = self.evaluate(node.func, env)
            args = [self.evaluate(a, env) for a in node.args]
            if isinstance(func, FlamePLFunction):
                return func.call(args, self)
            if isinstance(func, FlamePLClass):
                return func.call(args, self)
            if callable(func):
                return func(*args)
            raise TypeError(f"{func} is not callable")
        if isinstance(node, MethodCall):
            obj = self.evaluate(node.obj, env)
            args = [self.evaluate(a, env) for a in node.args]
            if isinstance(obj, FlamePLInstance):
                method = obj.get_method(node.method)
                if method:
                    return method.call(args, self)
            raise TypeError(f"No method {node.method} on {obj}")
        if isinstance(node, Index):
            obj = self.evaluate(node.obj, env)
            idx = self.evaluate(node.index, env)
            if isinstance(obj, list):
                return obj[idx]
            if isinstance(obj, dict):
                return obj[idx]
            raise TypeError(f"Cannot index {obj}")
        if isinstance(node, ListLiteral):
            return [self.evaluate(e, env) for e in node.elements]
        if isinstance(node, PairLiteral):
            return FlamePLPair(self.evaluate(node.first, env), self.evaluate(node.second, env))
        if isinstance(node, DictLiteral):
            d = {}
            for k, v in node.items:
                d[self.evaluate(k, env)] = self.evaluate(v, env)
            return d
        if isinstance(node, Print):
            val = self.evaluate(node.expr, env)
            print(val)
            return None
        if isinstance(node, Input):
            prompt = self.evaluate(node.prompt, env)
            result = input(str(prompt))
            return result
        if isinstance(node, TypeOf):
            val = self.evaluate(node.expr, env)
            if isinstance(val, bool):
                return BOOL_TYPE
            if isinstance(val, int) or isinstance(val, Decimal):
                return INT_TYPE if not isinstance(val, Decimal) or val.as_tuple().exponent >= 0 else FLOAT_TYPE
            if isinstance(val, float):
                return FLOAT_TYPE
            if isinstance(val, FlamePLComplex):
                return COMPLEX_TYPE
            if isinstance(val, list):
                return LIST_TYPE
            if isinstance(val, FlamePLPair):
                return PAIR_TYPE
            if isinstance(val, dict):
                return DICT_TYPE
            if isinstance(val, str):
                return STR_TYPE
            if val is None:
                return NULL_TYPE
            if isinstance(val, (FlamePLFunction, FlamePLBoundMethod)):
                return FUNCTION_TYPE
            if isinstance(val, (FlamePLClass, FlamePLInstance)):
                return CLASS_TYPE
            if isinstance(val, FlamePLModule):
                return MODULE_TYPE
            return FlamePLType(type(val).__name__)
        if isinstance(node, Convert):
            val = self.evaluate(node.expr, env)
            if node.kind == 'int':
                return int(val)
            if node.kind == 'float':
                return Decimal(str(val))
            if node.kind == 'str':
                return str(val)
            raise RuntimeError(f"Unknown conversion: {node.kind}")

        if isinstance(node, Import):
            mod_env = self.load_module(node.module_name, self.current_file_dir)
            mod_obj = FlamePLModule(mod_env, node.module_name)
            alias = node.alias if node.alias else node.module_name
            if not node.alias and isinstance(node.module_name, str):
                base = os.path.basename(node.module_name)
                if base.endswith('.flpl'):
                    alias = base[:-3]
                else:
                    alias = base
            env.set(alias, mod_obj)
            return mod_obj

        if isinstance(node, FromImport):
            mod_env = self.load_module(node.module_name, self.current_file_dir)
            for orig, alias in node.imports:
                try:
                    value = mod_env.get(orig)
                except NameError:
                    raise ImportError(f"Module {node.module_name} has no attribute {orig}")
                target = alias if alias else orig
                env.set(target, value)
            return None

        raise RuntimeError(f"Unknown AST node: {type(node)}")

    def evaluate_block(self, block, env):
        result = None
        for stmt in block:
            result = self.evaluate(stmt, env)
        return result

# ----------------------------------------------------------------------
#  Main entry
# ----------------------------------------------------------------------

def run_flame(source, filename="<stdin>"):
    base_dir = os.path.dirname(os.path.abspath(filename)) if filename != "<stdin>" else os.getcwd()

    lexer = Lexer(source)
    tokens = lexer.tokenize()
    parser = Parser(tokens)
    ast = parser.parse()

    global_env = Environment()
    def print_fn(*args):
        print(' '.join(str(a) for a in args))
    def input_fn(prompt):
        return input(str(prompt))
    global_env.define('print', print_fn)
    global_env.define('input', input_fn)

    evaluator = Evaluator(global_env)
    evaluator.current_file_dir = base_dir
    evaluator.evaluate_block(ast, global_env)

# ----------------------------------------------------------------------
#  Main runner
# ----------------------------------------------------------------------

if __name__ == "__main__":
    if len(sys.argv) > 1:
        filename = sys.argv[1]
        try:
            with open(filename, 'r') as f:
                source = f.read()
            run_flame(source, filename)
        except FileNotFoundError:
            print(f"Error: File '{filename}' not found.", file=sys.stderr)
            sys.exit(1)
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            sys.exit(1)
    else:
        source = sys.stdin.read()
        if not source:
            print("FlamePL Interpreter - usage:", file=sys.stderr)
            print("  python flame.py <filename>   to run a file", file=sys.stderr)
            print("  python flame.py              to read from stdin (pipe/redirect)", file=sys.stderr)
            sys.exit(0)
        run_flame(source, "<stdin>")