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
import re
import sys
import math
from decimal import Decimal, getcontext

# Set high precision for arbitrary‑precision floats
getcontext().prec = 80

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

class FlamePLType:
    """First‑class type objects."""
    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")      # <-- NEW boolean type

class FlamePLComplex:
    """Arbitrary‑precision complex number (real, imag as Decimal)."""
    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

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

# ----------------------------------------------------------------------
#  Environment (for variable lookup & closures)
# ----------------------------------------------------------------------

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):          # <-- NEW node for true/false
    def __init__(self, value):
        self.value = value  # True or False

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, else_block):
        self.cond = cond
        self.then_block = then_block
        self.else_block = else_block

class While(ASTNode):
    def __init__(self, cond, body):
        self.cond = cond
        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

# ----------------------------------------------------------------------
#  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

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

            # Imaginary literal: e.g. 4i, 10i
            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 (float or int)
            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

            # 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', 'end', 'while', 'do',
                    'return', 'null', 'lambda', 'and', 'or', 'not',
                    'true', 'false'                     # <-- booleans as keywords
                }
                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 == '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)
        # Assignment or expression
        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_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()
        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'):
                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', 'end'):
                break
            then_block.append(self.parse_statement())
        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, 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_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()
        if 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', '//'):
            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'):   # <-- handle booleans
            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

    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 Python bool
            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)         # <-- force boolean
            if op == 'or':
                return bool(left or right)          # <-- force boolean
            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                # already bool
            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)                # <-- force boolean
            raise RuntimeError(f"Unknown unary op: {node.op}")
        if isinstance(node, If):
            cond = self.evaluate(node.cond, env)
            if cond:
                return self.evaluate_block(node.then_block, env)
            else:
                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, 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):              # <-- recognize bool type
                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
            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}")

        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):
    lexer = Lexer(source)
    tokens = lexer.tokenize()
    parser = Parser(tokens)
    ast = parser.parse()

    global_env = Environment()
    # built‑in functions
    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.evaluate_block(ast, global_env)

# ----------------------------------------------------------------------
#  Updated sample program with booleans
# ----------------------------------------------------------------------

SAMPLE_PROGRAM = """
# 1. Arbitrary precision float
a <- 1.23456789012345678901234567890
b <- 2.0
c <- a * b
print("Arbitrary float result: " + to_str(c))

# 2. Lists, ordered pair, complex, dict
my_list <- [1, 2, 3, 4]
my_pair <- (10, 20)
my_complex <- 3 + 4i
my_dict <- {"name": "FlamePL", "version": 1.0}
print(my_list)
print(my_pair)
print(my_complex)
print(my_dict)

# 3. Input and output (commented out to run non‑interactively)
# name <- input("Enter your name: ")
# print("Hello, " + name)

# 4. Multi-line string
multiline <- """
This is line 1
This is line 2
This is line 3
"""
print(multiline)

# 5. OOP
class Animal
    fn init(name)
        self.name <- name
    end
    fn speak()
        print(self.name + " makes a noise.")
    end
end

class Dog : Animal
    fn speak()
        print(self.name + " barks!")
    end
end

d <- Dog("Rex")
d.speak()

# 6. Function, lambda, closures, recursion
fn factorial(n)
    if n = 0 then
        return 1
    else
        return n * factorial(n - 1)
    end
end
print("Factorial of 5: " + to_str(factorial(5)))

# Lambda
square <- lambda (x) -> x * x
print("Square of 6: " + to_str(square(6)))

# Closure
fn make_counter()
    count <- 0
    fn inner()
        count <- count + 1
        return count
    end
    return inner
end
counter <- make_counter()
print(counter())  # 1
print(counter())  # 2

# 7. Type convert
num_str <- "3.14159"
pi <- to_float(num_str)
print(pi)

# 8. Treat type itself as data type
t <- type_of(10)
if t = int then
    print("10 is an integer")
end

# 9. Null value
n <- null
print(n)  # prints null

# 10. Exponent ^, integer division //, comment #
res1 <- 2 ^ 10   # 1024
res2 <- 10 // 3  # 3
print("2^10 = " + to_str(res1))
print("10 // 3 = " + to_str(res2))

# 11. Logical operators in English (now return booleans)
if true and false then
    print("This won't print")
else
    print("Logical 'and' works")
end

if true or false then
    print("Logical 'or' works")
end

if not false then
    print("Logical 'not' works")
end

# 12. Assignment <-, equality =
x <- 5
if x = 5 then
    print("Assignment and equality check passed")
end

# ---- NEW: Demonstrate boolean literals and type ----
b1 <- true
b2 <- false
print("b1 = " + to_str(b1))
print("b2 = " + to_str(b2))
print("type_of(b1) = " + to_str(type_of(b1)))
print("type_of(b1) = bool? " + to_str(type_of(b1) = bool))

# Logical operators always return booleans
result_and <- true and false
result_or <- true or false
result_not <- not true
print("true and false = " + to_str(result_and))
print("true or false = " + to_str(result_or))
print("not true = " + to_str(result_not))
print("type_of(result_and) = " + to_str(type_of(result_and)))
"""

if __name__ == "__main__":
    # To read from a file, uncomment:
    # with open("program.flpl", "r") as f:
    #     run_flame(f.read())
    run_flame(SAMPLE_PROGRAM)