import re
import sys
import subprocess
import os

class CentellaCompiler:
    def __init__(self):
        self.ir = []
        self.label_id = 0
        self.var_id = 0
        self.str_id = 0
        self.var_map = {}
        self.global_strings = []
        self.allocations = []
        self.current_file_ptr = None # Nuevo: para rastrear archivos abiertos para escritura

    def new_label(self, name="L"):
        self.label_id += 1
        return f"{name}{self.label_id}"

    def new_var(self):
        self.var_id += 1
        return f"%v{self.var_id}"

    def emit(self, code):
        self.ir.append("  " + code)

    def tokenize(self, text):
        token_specification = [
            ('NUMBER',   r'\d+(\.\d+)?'),
            ('STRING',   r'"[^"]*"'), # Literales de string
            ('ID',       r'[a-zA-Z_]\w*'),
            ('OP_LOG',   r'==|!=|<=|>=|<|>'),
            ('OP_BOOL',  r'&&|\|\|'), # Nuevos operadores && y ||
            ('OP_ART',   r'[+\-*/%()]'),
            ('ASSIGN',   r'='),
            ('SEP',      r','),
            ('SKIP',     r'[ \t]+'),
            ('MISMATCH', r'.'),
        ]
        tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
        for mo in re.finditer(tok_regex, text):
            kind = mo.lastgroup
            if kind != 'SKIP' and kind != 'MISMATCH':
                yield mo.group(kind)

    def compile_expr(self, expr_text):
        tokens = list(self.tokenize(expr_text))
        pos = 0

        def peek(): return tokens[pos] if pos < len(tokens) else None
        def consume():
            nonlocal pos; t = peek(); pos += 1
            return t

        def factor():
            t = consume()
            if not t: return ("0.0", "double") # Retorna tupla (valor, tipo)
            
            if t in ('max', 'min', 'promedio'):
                func_name = t
                consume() # '('
                args = []
                while peek() != ')':
                    arg_val, arg_type = logic_or()
                    if arg_type != "double": raise Exception(f"Funcion {func_name} solo acepta numeros")
                    args.append(arg_val)
                    if peek() == ',': consume()
                consume() # ')'
                
                if len(args) < 1: raise Exception(f"Funcion {func_name} requiere argumentos")
                
                if func_name == 'max':
                    res = args[0]
                    for i in range(1, len(args)):
                        curr = args[i]
                        cond = self.new_var()
                        self.emit(f"{cond} = fcmp ogt double {res}, {curr}")
                        next_res = self.new_var()
                        self.emit(f"{next_res} = select i1 {cond}, double {res}, double {curr}")
                        res = next_res
                    return (res, "double")
                
                elif func_name == 'min':
                    res = args[0]
                    for i in range(1, len(args)):
                        curr = args[i]
                        cond = self.new_var()
                        self.emit(f"{cond} = fcmp olt double {res}, {curr}")
                        next_res = self.new_var()
                        self.emit(f"{next_res} = select i1 {cond}, double {res}, double {curr}")
                        res = next_res
                    return (res, "double")
                
                elif func_name == 'promedio':
                    sum_val = args[0]
                    for i in range(1, len(args)):
                        curr = args[i]
                        next_sum = self.new_var()
                        self.emit(f"{next_sum} = fadd double {sum_val}, {curr}")
                        sum_val = next_sum
                    
                    count = float(len(args))
                    res = self.new_var()
                    self.emit(f"{res} = fdiv double {sum_val}, {count:.6f}")
                    return (res, "double")

                    self.emit(f"{res} = fdiv double {sum_val}, {count:.6f}")
                    return (res, "double")

            if t in ('contiene', 'empieza_con'):
                func_name = t
                consume() # '('
                # Primer argumento: string
                str_val, str_type = logic_or()
                if str_type != "string": raise Exception(f"Primer argumento de {func_name} debe ser texto")
                
                if peek() != ',': raise Exception(f"Funcion {func_name} requiere dos argumentos")
                consume() # ','
                
                # Segundo argumento: string (pattern/prefix)
                pat_val, pat_type = logic_or()
                if pat_type != "string": raise Exception(f"Segundo argumento de {func_name} debe ser texto")
                
                consume() # ')'
                
                res = self.new_var()
                if func_name == 'contiene':
                    self.emit(f"{res} = call double @centella_str_contains(i8* {str_val}, i8* {pat_val})")
                else: 
                    self.emit(f"{res} = call double @centella_str_starts_with(i8* {str_val}, i8* {pat_val})")
                
                return (res, "double")

            if t == 'raiz':
                # raiz(expr)
                val, type = comparison()
                if type != "double": raise Exception("raiz solo acepta numeros")
                res = self.new_var()
                self.emit(f"{res} = call double @llvm.sqrt.f64(double {val})")
                return (res, "double")
            
            if t.startswith('"'):
                # String literal
                content = t[1:-1]
                self.str_id += 1
                str_name = f"@.str.lit.{self.str_id}"
                raw_bytes = content.encode('utf-8')
                llvm_str = "".join(f"\\{b:02X}" if b < 32 or b > 126 or b == 34 or b == 92 else chr(b) for b in raw_bytes) + "\\00"
                str_size = len(raw_bytes) + 1
                self.global_strings.append(f'{str_name} = private unnamed_addr constant [{str_size} x i8] c"{llvm_str}", align 1')
                ptr = self.new_var()
                self.emit(f"{ptr} = getelementptr [{str_size} x i8], [{str_size} x i8]* {str_name}, i64 0, i64 0")
                return (ptr, "string")

            if re.match(r'^\d+(\.\d+)?$', t):
                return ("{:.6f}".format(float(t)), "double")
            
            if t == '(':
                val, type = logic_or() # Paréntesis reinician precedencia
                consume() # ')'
                return (val, type)
            
            if t in self.var_map:
                info = self.var_map[t] # (pointer, type)
                if info['type'] == 'double':
                    temp = self.new_var()
                    self.emit(f"{temp} = load double, double* {info['ptr']}, align 8")
                    return (temp, "double")
                elif info['type'] == 'string':
                    # Para strings, devolvemos el puntero al buffer (decay to pointer)
                    temp = self.new_var()
                    self.emit(f"{temp} = getelementptr [256 x i8], [256 x i8]* {info['ptr']}, i64 0, i64 0")
                    return (temp, "string")
                
            return ("0.0", "double")

        def mul_expr():
            left_val, left_type = factor()
            while peek() in ('*', '/', '%'):
                op = consume()
                right_val, right_type = factor()
                if left_type != "double" or right_type != "double":
                     raise Exception("Operaciones aritmeticas solo en numeros")
                temp = self.new_var()
                if op == '*': cmd = "fmul"
                elif op == '/': cmd = "fdiv"
                else: cmd = "frem"
                self.emit(f"{temp} = {cmd} double {left_val}, {right_val}")
                left_val = temp
                left_type = "double"
            return (left_val, left_type)

        def add_expr():
            left_val, left_type = mul_expr()
            while peek() in ('+', '-'):
                op = consume()
                right_val, right_type = mul_expr()
                if left_type != "double" or right_type != "double":
                     raise Exception("Operaciones aritmeticas solo en numeros")
                temp = self.new_var()
                cmd = "fadd" if op == "+" else "fsub"
                self.emit(f"{temp} = {cmd} double {left_val}, {right_val}")
                left_val = temp
                left_type = "double"
            return (left_val, left_type)

        def comparison():
            left_val, left_type = add_expr()
            ops_map = {'==': 'oeq', '!=': 'one', '<': 'olt', '<=': 'ole', '>': 'ogt', '>=': 'oge'}
            if peek() in ops_map:
                op_token = consume()
                right_val, right_type = add_expr()
                
                cmp_res = self.new_var()
                
                if left_type == 'string' and right_type == 'string':
                    # Comparacion de strings usando Runtime
                    if op_token == '==':
                        self.emit(f"{cmp_res} = call double @centella_str_eq(i8* {left_val}, i8* {right_val})")
                    elif op_token == '!=':
                        self.emit(f"{cmp_res} = call double @centella_str_ne(i8* {left_val}, i8* {right_val})")
                    else:
                        raise Exception("Strings solo soportan == y !=")
                    return (cmp_res, "double") # Retorna 1.0 o 0.0
                
                elif left_type == 'double' and right_type == 'double':
                    res_i1 = self.new_var()
                    self.emit(f"{res_i1} = fcmp {ops_map[op_token]} double {left_val}, {right_val}")
                    self.emit(f"{cmp_res} = uitofp i1 {res_i1} to double")
                    return (cmp_res, "double")
                else:
                    raise Exception("Tipos incompatibles en comparacion")
            return (left_val, left_type)
        
        # --- NUEVO: Operadores Logicos ---
        
        def logic_and():
            left_val, left_type = comparison()
            while peek() == '&&':
                consume()
                # Short-circuiting AND
                # Si left es falso (0.0), saltar al final devolviendo 0.0
                # Si left es verdadero, evaluar right.
                
                # Convertir left a bool context
                cond_l = self.new_var()
                self.emit(f"{cond_l} = fcmp one double {left_val}, 0.0")
                
                label_check_right = self.new_label("and_check")
                label_end = self.new_label("and_end")
                res_ptr = self.new_var()
                
                # Allocamos espacio temporal para el resultado (phi nodes son complejos en one-pass)
                self.emit(f"{res_ptr} = alloca double") 
                self.emit(f"store double 0.0, double* {res_ptr}") # Default false
                
                self.emit(f"br i1 {cond_l}, label %{label_check_right}, label %{label_end}")
                
                self.ir.append(f"{label_check_right}:")
                right_val, right_type = comparison()
                # Guardar resultado de right (0.0 o 1.0 si es comparacion, o valor si es numero)
                # Normalizar a booleano 0.0/1.0
                cond_r = self.new_var()
                res_dbl = self.new_var()
                self.emit(f"{cond_r} = fcmp one double {right_val}, 0.0")
                self.emit(f"{res_dbl} = uitofp i1 {cond_r} to double")
                self.emit(f"store double {res_dbl}, double* {res_ptr}")
                self.emit(f"br label %{label_end}")
                
                self.ir.append(f"{label_end}:")
                left_val = self.new_var()
                self.emit(f"{left_val} = load double, double* {res_ptr}")
                left_type = "double"
                
            return (left_val, left_type)

        def logic_or():
            left_val, left_type = logic_and() # Precedencia: OR < AND
            while peek() == '||':
                consume()
                # Short-circuiting OR
                # Si left es true (!= 0.0), saltar al final devolviendo 1.0 (o el valor)
                
                cond_l = self.new_var()
                self.emit(f"{cond_l} = fcmp one double {left_val}, 0.0")
                
                label_check_right = self.new_label("or_check")
                label_end = self.new_label("or_end")
                res_ptr = self.new_var()
                
                self.emit(f"{res_ptr} = alloca double")
                # Si es true, guardamos 1.0
                self.emit(f"store double 1.0, double* {res_ptr}")
                
                self.emit(f"br i1 {cond_l}, label %{label_end}, label %{label_check_right}")
                
                self.ir.append(f"{label_check_right}:")
                right_val, right_type = logic_and()
                cond_r = self.new_var()
                res_dbl = self.new_var()
                self.emit(f"{cond_r} = fcmp one double {right_val}, 0.0")
                self.emit(f"{res_dbl} = uitofp i1 {cond_r} to double")
                self.emit(f"store double {res_dbl}, double* {res_ptr}")
                self.emit(f"br label %{label_end}")
                
                self.ir.append(f"{label_end}:")
                left_val = self.new_var()
                self.emit(f"{left_val} = load double, double* {res_ptr}")
                left_type = "double"
                
            return (left_val, left_type)

        return logic_or() # Entrada principal de expresiones

    def compile_print(self, content, target="stdout"):
        parts = re.split(r'(".*?")', content)
        format_str = ""
        args = []
        for p in (p.strip() for p in parts if p.strip()):
            if p.startswith('"'): format_str += p[1:-1]
            else:
                val, type = self.compile_expr(p)
                if type == "string":
                    format_str += "%s"
                    args.append(f"i8* {val}")
                else:
                    format_str += "%f"
                    args.append(f"double {val}")
        format_str += "\n"
        
        self.str_id += 1
        str_name = f"@.str.{self.str_id}"
        raw_bytes = format_str.encode('utf-8')
        llvm_str = "".join(f"\\{b:02X}" if b < 32 or b > 126 or b == 34 or b == 92 else chr(b) for b in raw_bytes) + "\\00"
        str_size = len(raw_bytes) + 1
        
        self.global_strings.append(f'{str_name} = private unnamed_addr constant [{str_size} x i8] c"{llvm_str}", align 1')
        ptr = self.new_var()
        self.emit(f"{ptr} = getelementptr [{str_size} x i8], [{str_size} x i8]* {str_name}, i64 0, i64 0")
        
        # Lógica para elegir si imprimimos en pantalla o en un archivo
        if target == "file" and self.current_file_ptr:
            # centella_write(file_ptr, format, args...)
            arg_list = ", ".join(["i8* " + self.current_file_ptr, "i8* " + ptr] + args)
            self.emit(f"call void (i8*, i8*, ...) @centella_write({arg_list})")
        else:
            # printf(format, args...)
            arg_list = ", ".join(["i8* " + ptr] + args)
            self.emit(f"call i32 (i8*, ...) @printf({arg_list})")

    def process_block(self, lines, index):
        depth = 1; block_lines = []; i = index
        while i < len(lines) and depth > 0:
            line = lines[i].strip()
            if not line: i += 1; continue
            if '{' in line: depth += 1
            if '}' in line: 
                depth -= 1
                if depth == 0: break
            block_lines.append(line)
            i += 1
        return block_lines, i

    def compile_lines(self, lines):
        i = 0
        while i < len(lines):
            line = lines[i].strip()
            if not line or line.startswith('//'): i += 1; continue
            
            # --- GUARDAR DATOS (HIBRIDO) ---
            # guardar "archivo.csv" { ... }
            m_guardar = re.match(r'guardar\s+"(.+?)"\s*\{', line)
            if m_guardar:
                filename = m_guardar.group(1)
                
                # Strings const
                self.str_id += 1
                fn_str = f"@.str.fn.{self.str_id}"
                self.global_strings.append(f'{fn_str} = private unnamed_addr constant [{len(filename)+1} x i8] c"{filename}\\00", align 1')
                fn_ptr = self.new_var()
                self.emit(f"{fn_ptr} = getelementptr [{len(filename)+1} x i8], [{len(filename)+1} x i8]* {fn_str}, i64 0, i64 0")

                self.str_id += 1
                mode_str = f"@.str.mode.{self.str_id}"
                self.global_strings.append(f'{mode_str} = private unnamed_addr constant [2 x i8] c"w\\00", align 1')
                mode_ptr = self.new_var()
                self.emit(f"{mode_ptr} = getelementptr [2 x i8], [2 x i8]* {mode_str}, i64 0, i64 0")

                # Open
                fp = self.new_var()
                self.emit(f"{fp} = call i8* @centella_open(i8* {fn_ptr}, i8* {mode_ptr})")

                # Set current file ptr
                prev_fp = self.current_file_ptr
                self.current_file_ptr = fp

                # Process block
                block_lines, next_i = self.process_block(lines, i + 1)
                self.compile_lines(block_lines)

                # Close
                self.emit(f"call void @centella_close(i8* {fp})")
                self.current_file_ptr = prev_fp
                
                i = next_i + 1; continue

            # --- LECTURA DE DATOS MASIVA (HIBRIDA) ---
            # procesar "archivo" capturando (id, nombre: texto, precio)
            m_data = re.match(r'procesar\s+"(.+?)"\s+capturando\s+\((.+?)\)\s*\{', line)
            if m_data:
                filename, vars_str = m_data.groups()
                # Parsear variables y tipos: "id, nombre: texto, precio" -> [("id", "double"), ("nombre", "string")]
                vars_def = []
                for v_chunk in vars_str.split(','):
                    v_chunk = v_chunk.strip()
                    if ':' in v_chunk:
                        v_name, v_type_alias = v_chunk.split(':')
                        v_name = v_name.strip()
                        v_type = "string" if v_type_alias.strip() == "texto" else "double"
                    else:
                        v_name = v_chunk
                        v_type = "double"
                    vars_def.append((v_name, v_type))

                # Allocamos las variables
                for v_name, v_type in vars_def:
                    if v_name not in self.var_map:
                        vreg = self.new_var()
                        self.var_map[v_name] = {'ptr': vreg, 'type': v_type}
                        if v_type == "double":
                            self.allocations.append(f"  {vreg} = alloca double, align 8")
                        else:
                            # Strings fijos de 256 bytes para simplificar
                            self.allocations.append(f"  {vreg} = alloca [256 x i8], align 16")

                # Preparar strings constantes
                self.str_id += 1
                fn_str = f"@.str.fn.{self.str_id}"
                self.global_strings.append(f'{fn_str} = private unnamed_addr constant [{len(filename)+1} x i8] c"{filename}\\00", align 1')
                fn_ptr = self.new_var()
                self.emit(f"{fn_ptr} = getelementptr [{len(filename)+1} x i8], [{len(filename)+1} x i8]* {fn_str}, i64 0, i64 0")

                self.str_id += 1
                mode_str = f"@.str.mode.{self.str_id}"
                self.global_strings.append(f'{mode_str} = private unnamed_addr constant [2 x i8] c"r\\00", align 1')
                mode_ptr = self.new_var()
                self.emit(f"{mode_ptr} = getelementptr [2 x i8], [2 x i8]* {mode_str}, i64 0, i64 0")

                # Llamar a centella_open del Runtime
                fp = self.new_var()
                self.emit(f"{fp} = call i8* @centella_open(i8* {fn_ptr}, i8* {mode_ptr})")

                # Construir string de formato de tipos para el Runtime (ej: "nsn" -> numero, string, numero)
                fmt_types = "".join(['n' if vt == 'double' else 's' for _, vt in vars_def])
                self.str_id += 1
                fmt_str = f"@.str.fmt.{self.str_id}"
                self.global_strings.append(f'{fmt_str} = private unnamed_addr constant [{len(fmt_types)+1} x i8] c"{fmt_types}\\00", align 1')
                fmt_ptr = self.new_var()
                self.emit(f"{fmt_ptr} = getelementptr [{len(fmt_types)+1} x i8], [{len(fmt_types)+1} x i8]* {fmt_str}, i64 0, i64 0")

                l_loop, l_body, l_end = self.new_label("loop"), self.new_label("body"), self.new_label("end")
                self.emit(f"br label %{l_loop}")
                self.ir.append(f"{l_loop}:")

                # Preparar argumentos para centella_read_row
                # Argumentos variables: punteros a donde guardar los datos
                runtime_args = []
                for v_name, v_type in vars_def:
                    info = self.var_map[v_name]
                    if v_type == "double":
                         runtime_args.append(f"double* {info['ptr']}")
                    else:
                         # Pasar puntero al inicio del array char
                         ptr_to_arr_start = self.new_var()
                         self.emit(f"{ptr_to_arr_start} = getelementptr [256 x i8], [256 x i8]* {info['ptr']}, i64 0, i64 0")
                         runtime_args.append(f"i8* {ptr_to_arr_start}")
                
                args_str = ", ".join(runtime_args)
                res = self.new_var()
                # call i32 (i8*, i8*, ...) @centella_read_row(fp, fmt, vars...)
                self.emit(f"{res} = call i32 (i8*, i8*, ...) @centella_read_row(i8* {fp}, i8* {fmt_ptr}, {args_str})")
                
                cond_loop = self.new_var()
                self.emit(f"{cond_loop} = icmp ne i32 {res}, 0")
                self.emit(f"br i1 {cond_loop}, label %{l_body}, label %{l_end}")

                self.ir.append(f"{l_body}:")
                block_lines, next_i = self.process_block(lines, i + 1)
                self.compile_lines(block_lines)
                self.emit(f"br label %{l_loop}")

                self.ir.append(f"{l_end}:")
                self.emit(f"call void @centella_close(i8* {fp})")
                i = next_i + 1; continue

            # --- CONDICIONALES ---
            if line.startswith('si '):
                match = re.match(r'si\s+(.+?)\s*\{', line)
                if match:
                    cond_val, _ = self.compile_expr(match.group(1))
                    res = self.new_var()
                    self.emit(f"{res} = fcmp one double {cond_val}, 0.0")
                    l_then, l_else, l_merge = self.new_label("then"), self.new_label("else"), self.new_label("merge")
                    self.emit(f"br i1 {res}, label %{l_then}, label %{l_else}")
                    self.ir.append(f"{l_then}:")
                    then_block, next_i = self.process_block(lines, i + 1)
                    self.compile_lines(then_block)
                    self.emit(f"br label %{l_merge}")
                    self.ir.append(f"{l_else}:")
                    i = next_i
                    if i < len(lines) and "sino" in lines[i]:
                        else_block, next_i = self.process_block(lines, i + 1)
                        self.compile_lines(else_block)
                        i = next_i
                    self.emit(f"br label %{l_merge}")
                    self.ir.append(f"{l_merge}:")
                    i += 1; continue
            
            # --- ASIGNACION ---
            m = re.match(r'(?:sea\s+)?([a-zA-Z_]\w*)\s*=\s*(.+)', line)
            if m:
                var, expr = m.groups()
                val, type = self.compile_expr(expr)
                
                if var not in self.var_map:
                    vreg = self.new_var()
                    self.var_map[var] = {'ptr': vreg, 'type': type}
                    if type == "double":
                        self.allocations.append(f"  {vreg} = alloca double, align 8")
                    else:
                        self.allocations.append(f"  {vreg} = alloca [256 x i8], align 16")
                
                info = self.var_map[var]
                if info['type'] != type:
                    raise Exception(f"Type mismatch: variable {var} es {info['type']} pero asignaste {type}")
                
                if type == "double":
                    self.emit(f"store double {val}, double* {info['ptr']}, align 8")
                else: 
                    # String copy manual en LLVM? -> Mejor usar strcpy del runtime/libc o memcpy
                    # Usaremos llvm.memcpy
                    dest = self.new_var()
                    self.emit(f"{dest} = getelementptr [256 x i8], [256 x i8]* {info['ptr']}, i64 0, i64 0")
                    # Asumimos que val es i8* (char*)
                    # Declarar strcpy
                    self.emit(f"call i8* @strcpy(i8* {dest}, i8* {val})")

            elif line.startswith('imprimir '): 
                self.compile_print(line[9:], target="stdout")
            elif line.startswith('escribir '): 
                self.compile_print(line[9:], target="file")
            
            i += 1

    def generate_llvm(self, source):
        self.compile_lines(source.splitlines())
        
        header = [
            '; Compilado por Centella-Data V4 (Hybrid Runtime)',
            'declare i32 @printf(i8*, ...)',
            'declare double @llvm.sqrt.f64(double)',
            # Funciones del Runtime C
            'declare i8* @centella_open(i8*, i8*)',
            'declare void @centella_close(i8*)',
            'declare i32 @centella_read_row(i8*, i8*, ...)',
            'declare void @centella_write(i8*, i8*, ...)',
            'declare double @centella_str_eq(i8*, i8*)',
            'declare double @centella_str_ne(i8*, i8*)',
            'declare double @centella_str_contains(i8*, i8*)',
            'declare double @centella_str_starts_with(i8*, i8*)',
            # LibC standard
            'declare i8* @strcpy(i8*, i8*)',
            *self.global_strings,
            'define i32 @main() {',
            'entry:'
        ] + self.allocations
        return "\n".join(header + self.ir + ['  ret i32 0', '}'])

def main():
    if len(sys.argv) < 2:
        print("❌ Uso: centella <archivo.centella>")
        sys.exit(1)
        
    input_file = sys.argv[1]
    base_name = os.path.splitext(os.path.basename(input_file))[0]
    
    # 1. Compilar Runtime C
    print("🔨 Compilando Runtime...")
    try:
        if os.name == 'nt':
             # En Windows, clang suele necesitar saber el target si se mezcla con MinGW o MSVC
             # Usaremos el mismo target 
             subprocess.run(['clang', '-c', 'runtime.c', '-o', 'runtime.o', 
                             '--target=x86_64-pc-windows-gnu'], check=True)
        else:
             subprocess.run(['clang', '-c', 'runtime.c', '-o', 'runtime.o'], check=True)
    except Exception as e:
        print(f"❌ Error compilando runtime: {e}")
        sys.exit(1)

    # 2. Generar LLVM IR
    print("📝 Generando Codigo Intermedio...")
    with open(input_file, 'r', encoding='utf-8') as f:
        source = f.read()
        
    compiler = CentellaCompiler()
    try:
        llvm_ir = compiler.generate_llvm(source)
    except Exception as e:
        print(f"❌ Error de compilacion: {e}")
        sys.exit(1)
    
    with open(f"{base_name}.ll", "w", encoding='utf-8') as f:
        f.write(llvm_ir)
        
    # 3. Enlazar (Link)
    print("🔗 Enlazando...")
    # Ubicacion del runtime (mismo directorio que el compilador)
    script_dir = os.path.dirname(os.path.abspath(__file__))
    runtime_obj = os.path.join(script_dir, "runtime.o")
    runtime_src = os.path.join(script_dir, "runtime.c")

    # Compilar runtime si no existe
    if not os.path.exists(runtime_obj):
        print("⚙️ Compilando Runtime C...")
        subprocess.run(['clang', '-c', runtime_src, '-o', runtime_obj], check=True)

    if os.name == 'nt':
        comando_clang = [
            'clang', '-O3', 
            '--target=x86_64-pc-windows-gnu', 
            f"{base_name}.ll", runtime_obj,
            '-o', f"{base_name}.exe"
        ]
        ejecutable = f".\\{base_name}.exe"
    else:
        comando_clang = ['clang', '-O3', f"{base_name}.ll", runtime_obj, '-o', base_name, '-lm']
        ejecutable = f"./{base_name}"
        
    try:
        subprocess.run(comando_clang, check=True)
        print(f"🚀 Centella-Data compilado con éxito: {ejecutable}")
    except subprocess.CalledProcessError:
        print("❌ Error durante el enlazado.")
        sys.exit(1)

if __name__ == "__main__":
    main()