import httpx from app.config import settings # Mapping Judge0 language_id to Piston language name and version # Updated with latest available versions in Piston LANGUAGE_MAP = { # Python 71: ("python", "3.12.0"), # Python 3.12 LTS 70: ("python", "3.12.0"), # Python 2 -> redirect to Python 3 # C/C++ 50: ("c", "10.2.0"), # C (GCC 10.2.0) 54: ("c++", "10.2.0"), # C++ (GCC 10.2.0) # Java 62: ("java", "15.0.2"), # Java (OpenJDK 15.0.2) # JavaScript/Node.js 63: ("javascript", "20.11.1"), # Node.js 20.11.1 # Go 60: ("go", "1.16.2"), # Go 1.16.2 # Rust 73: ("rust", "1.68.2"), # Rust 1.68.2 # Kotlin 78: ("kotlin", "1.8.20"), # Kotlin 1.8.20 } class JudgeStatus: """Status codes compatible with Judge0 format""" IN_QUEUE = 1 PROCESSING = 2 ACCEPTED = 3 WRONG_ANSWER = 4 TIME_LIMIT_EXCEEDED = 5 COMPILATION_ERROR = 6 RUNTIME_ERROR_SIGSEGV = 7 RUNTIME_ERROR_SIGXFSZ = 8 RUNTIME_ERROR_SIGFPE = 9 RUNTIME_ERROR_SIGABRT = 10 RUNTIME_ERROR_NZEC = 11 RUNTIME_ERROR_OTHER = 12 INTERNAL_ERROR = 13 EXEC_FORMAT_ERROR = 14 class JudgeService: """Code execution service using Piston API""" def __init__(self): self.base_url = settings.piston_url async def get_languages(self) -> list[dict]: """Get list of supported languages from Piston""" async with httpx.AsyncClient() as client: response = await client.get(f"{self.base_url}/api/v2/runtimes") response.raise_for_status() runtimes = response.json() # Create a set of available runtimes for quick lookup available = {rt["language"] for rt in runtimes} # Return only languages from LANGUAGE_MAP that are installed result = [] seen_ids = set() for lang_id, (piston_lang, piston_ver) in LANGUAGE_MAP.items(): if piston_lang in available and lang_id not in seen_ids: seen_ids.add(lang_id) # Find actual installed version actual_version = piston_ver for rt in runtimes: if rt["language"] == piston_lang: actual_version = rt["version"] break # Human-readable names name_map = { "python": "Python", "c": "C (GCC)", "c++": "C++ (GCC)", "java": "Java", "javascript": "JavaScript (Node.js)", "go": "Go", "rust": "Rust", "kotlin": "Kotlin", } display_name = name_map.get(piston_lang, piston_lang.title()) result.append({ "id": lang_id, "name": f"{display_name} ({actual_version})", }) return result def _normalize_output(self, output: str) -> str: """ Normalize output for comparison: - Strip trailing whitespace from each line - Ensure single trailing newline (as print() adds) """ if not output: return "" lines = output.splitlines() normalized = "\n".join(line.rstrip() for line in lines) if normalized: normalized += "\n" return normalized def _get_piston_language(self, language_id: int) -> tuple[str, str]: """Convert Judge0 language_id to Piston language name and version""" if language_id in LANGUAGE_MAP: return LANGUAGE_MAP[language_id] # Default to Python if unknown return ("python", "3.10.0") async def submit( self, source_code: str, language_id: int, stdin: str = "", expected_output: str = "", cpu_time_limit: float = 1.0, memory_limit: int = 262144, ) -> dict: """ Execute code using Piston and return result in Judge0-compatible format. """ language, version = self._get_piston_language(language_id) normalized_expected = self._normalize_output(expected_output) # Determine file extension ext_map = { "python": "py", "c": "c", "c++": "cpp", "java": "java", "javascript": "js", "go": "go", "rust": "rs", "kotlin": "kt", } ext = ext_map.get(language, "txt") filename = f"main.{ext}" # For Java, extract class name from source if language == "java": import re match = re.search(r'public\s+class\s+(\w+)', source_code) if match: filename = f"{match.group(1)}.java" async with httpx.AsyncClient(timeout=30.0) as client: try: response = await client.post( f"{self.base_url}/api/v2/execute", json={ "language": language, "version": version, "files": [ { "name": filename, "content": source_code, } ], "stdin": stdin, "run_timeout": int(cpu_time_limit * 1000), # Convert to ms "compile_timeout": 10000, # 10 seconds for compilation }, ) response.raise_for_status() result = response.json() except httpx.HTTPStatusError as e: return { "status": {"id": JudgeStatus.INTERNAL_ERROR, "description": "Internal Error"}, "stdout": "", "stderr": str(e), "compile_output": "", "message": f"Piston API error: {e.response.status_code}", "time": None, "memory": None, } except Exception as e: return { "status": {"id": JudgeStatus.INTERNAL_ERROR, "description": "Internal Error"}, "stdout": "", "stderr": str(e), "compile_output": "", "message": str(e), "time": None, "memory": None, } # Convert Piston response to Judge0-compatible format run_result = result.get("run", {}) compile_result = result.get("compile", {}) stdout = run_result.get("stdout", "") or "" stderr = run_result.get("stderr", "") or "" compile_output = compile_result.get("output", "") or "" exit_code = run_result.get("code", 0) signal = run_result.get("signal") # Determine status if compile_result.get("code") is not None and compile_result.get("code") != 0: # Compilation error status_id = JudgeStatus.COMPILATION_ERROR status_desc = "Compilation Error" elif signal == "SIGKILL": # Usually means timeout or memory limit status_id = JudgeStatus.TIME_LIMIT_EXCEEDED status_desc = "Time Limit Exceeded" elif signal is not None: # Runtime error with signal signal_map = { "SIGSEGV": JudgeStatus.RUNTIME_ERROR_SIGSEGV, "SIGFPE": JudgeStatus.RUNTIME_ERROR_SIGFPE, "SIGABRT": JudgeStatus.RUNTIME_ERROR_SIGABRT, } status_id = signal_map.get(signal, JudgeStatus.RUNTIME_ERROR_OTHER) status_desc = f"Runtime Error ({signal})" elif exit_code != 0: # Non-zero exit code status_id = JudgeStatus.RUNTIME_ERROR_NZEC status_desc = "Runtime Error (NZEC)" else: # Execution successful - compare output actual_output = self._normalize_output(stdout) if actual_output == normalized_expected: status_id = JudgeStatus.ACCEPTED status_desc = "Accepted" else: status_id = JudgeStatus.WRONG_ANSWER status_desc = "Wrong Answer" return { "status": {"id": status_id, "description": status_desc}, "stdout": stdout, "stderr": stderr, "compile_output": compile_output, "message": "", "time": None, # Piston doesn't provide execution time in same format "memory": None, # Piston doesn't provide memory usage } judge_service = JudgeService()