Ctrl K
ComunidadGuías…

compiler.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
Madaros compiler bridge.

Auto-detects environment:
- If MADAROS_BIN exists locally → runs madaros directly (remote workspace)
- Otherwise → tunnels via SSH to sounio-workspace-control (local Mac)
"""

import asyncio
import os
import subprocess
import threading
import time
from dataclasses import dataclass, field

_DEFAULT_BIN = "/workspace/sounio/bin/madaros"
MADAROS_BIN  = (os.environ.get("SOUNIO_MADAROS_BIN") or
                os.environ.get("MADAROS_RAW_BIN") or
                _DEFAULT_BIN)
SSH_HOST    = "sounio-workspace-control"
SSH_SOCKET  = os.path.expanduser("~/.ssh/sockets/sounio-workspace-control.sock")

# True when running on a host that has the madaros binary locally
_LOCAL = os.path.isfile(MADAROS_BIN)


@dataclass
class CheckResult:
    ok: bool
    time_ms: float
    raw: str
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)


def _parse_output(raw: str, exit_code: int) -> tuple[bool, list[str], list[str]]:
    errors, warnings = [], []
    for line in raw.splitlines():
        l = line.strip()
        if not l:
            continue
        low = l.lower()
        if "error" in low or "failed" in low or "unexpected" in low:
            errors.append(l)
        elif "warning" in low or "warn" in low:
            warnings.append(l)
    ok = exit_code == 0 and "check: OK" in raw
    return ok, errors, warnings


def _tmp_path() -> str:
    tid = threading.get_ident() % 99991
    return f"/tmp/mc_{os.getpid()}_{tid}.sio"


class MadarosCompiler:
    def __init__(self, madaros_bin: str = MADAROS_BIN,
                 ssh_host: str = SSH_HOST, timeout: float = 15.0):
        self.madaros_bin = madaros_bin
        self.ssh_host    = ssh_host
        self.timeout     = timeout

    def check(self, code: str) -> CheckResult:
        t0 = time.monotonic()
        if _LOCAL:
            return self._check_local(code, t0)
        return self._check_ssh(code, t0)

    def _check_local(self, code: str, t0: float) -> CheckResult:
        path = _tmp_path()
        try:
            with open(path, "w") as f:
                f.write(code)
            proc = subprocess.run(
                [self.madaros_bin, "check", path],
                capture_output=True, timeout=self.timeout,
            )
            elapsed = (time.monotonic() - t0) * 1000
        except subprocess.TimeoutExpired:
            return CheckResult(ok=False, time_ms=self.timeout * 1000,
                               raw="timeout", errors=["Timeout"])
        except Exception as e:
            return CheckResult(ok=False, time_ms=0, raw=str(e), errors=[str(e)])
        finally:
            try:
                os.unlink(path)
            except OSError:
                pass
        raw = proc.stdout.decode() + proc.stderr.decode()
        ok, errors, warnings = _parse_output(raw, proc.returncode)
        return CheckResult(ok=ok, time_ms=elapsed, raw=raw,
                           errors=errors, warnings=warnings)

    def _check_ssh(self, code: str, t0: float) -> CheckResult:
        import shlex
        path    = _tmp_path()
        inline  = shlex.quote(code)
        cmd_str = (f"printf %s {inline} > {path} && "
                   f"{self.madaros_bin} check {path}; rm -f {path}")
        ssh_cmd = ["ssh", "-S", SSH_SOCKET, self.ssh_host, cmd_str]
        try:
            proc = subprocess.run(ssh_cmd, capture_output=True, timeout=self.timeout)
            elapsed = (time.monotonic() - t0) * 1000
        except subprocess.TimeoutExpired:
            return CheckResult(ok=False, time_ms=self.timeout * 1000,
                               raw="timeout", errors=["Timeout"])
        except Exception as e:
            return CheckResult(ok=False, time_ms=0, raw=str(e), errors=[str(e)])
        raw = proc.stdout.decode() + proc.stderr.decode()
        ok, errors, warnings = _parse_output(raw, proc.returncode)
        return CheckResult(ok=ok, time_ms=elapsed, raw=raw,
                           errors=errors, warnings=warnings)


class AsyncMadarosCompiler:
    def __init__(self, madaros_bin: str = MADAROS_BIN,
                 ssh_host: str = SSH_HOST,
                 max_concurrent: int = 20, timeout: float = 15.0):
        # Local: 20 concurrent fine (no SSH overhead)
        # Remote via SSH: cap at 6 to avoid ControlMaster overload
        concurrency     = max_concurrent if _LOCAL else min(max_concurrent, 6)
        self.semaphore  = asyncio.Semaphore(concurrency)
        self.madaros_bin = madaros_bin
        self.ssh_host   = ssh_host
        self.timeout    = timeout

    async def check(self, code: str) -> CheckResult:
        async with self.semaphore:
            loop = asyncio.get_event_loop()
            c    = MadarosCompiler(self.madaros_bin, self.ssh_host, self.timeout)
            return await loop.run_in_executor(None, c.check, code)

    async def check_batch(self, codes: list[str]) -> list[CheckResult]:
        return await asyncio.gather(*[self.check(c) for c in codes])


def smoke_test():
    mode = "LOCAL" if _LOCAL else f"SSH→{SSH_HOST}"
    print(f"mode: {mode}")
    c = MadarosCompiler()
    for name, code in [
        ("valid",   "fn main() -> i32 { return 42 }"),
        ("invalid", "fn broken { ??? }"),
    ]:
        r = c.check(code)
        print(f"[{name}] ok={r.ok} time={r.time_ms:.0f}ms")
        for e in r.errors:
            print(f"  ERR: {e}")


if __name__ == "__main__":
    smoke_test()