| const DEFAULT_PISTON_EXECUTE_URL = 'https://emkc.org/api/v2/piston/execute'; |
| const REQUEST_TIMEOUT_MS = 15000; |
| const RUN_TIMEOUT_MS = 10000; |
| const COMPILE_TIMEOUT_MS = 10000; |
|
|
| const LANGUAGE_RUNTIMES = { |
| python: { |
| pistonLanguage: 'python', |
| version: '3.10.0', |
| fileName: 'main.py', |
| }, |
| cpp: { |
| pistonLanguage: 'c++', |
| version: '10.2.0', |
| fileName: 'main.cpp', |
| }, |
| java: { |
| pistonLanguage: 'java', |
| version: '15.0.2', |
| fileName: 'Main.java', |
| }, |
| javascript: { |
| pistonLanguage: 'javascript', |
| version: '18.15.0', |
| fileName: 'main.js', |
| }, |
| c: { |
| pistonLanguage: 'c', |
| version: '10.2.0', |
| fileName: 'main.c', |
| }, |
| }; |
|
|
| const LANGUAGE_ALIASES = { |
| py: 'python', |
| python3: 'python', |
| 'c++': 'cpp', |
| js: 'javascript', |
| node: 'javascript', |
| }; |
|
|
| export class CompilerError extends Error { |
| constructor(message, status, statusCode = 500, executionTime = 0) { |
| super(message); |
| this.name = 'CompilerError'; |
| this.status = status; |
| this.statusCode = statusCode; |
| this.executionTime = executionTime; |
| } |
| } |
|
|
| export function getSupportedLanguages() { |
| return Object.keys(LANGUAGE_RUNTIMES); |
| } |
|
|
| function normalizeLanguage(language) { |
| const key = String(language ?? '').trim().toLowerCase(); |
| return LANGUAGE_ALIASES[key] ?? key; |
| } |
|
|
| function getRuntime(language) { |
| const normalized = normalizeLanguage(language); |
| return LANGUAGE_RUNTIMES[normalized] ?? null; |
| } |
|
|
| function toText(value) { |
| return typeof value === 'string' ? value : ''; |
| } |
|
|
| function combinedStream(stream) { |
| if (!stream) { |
| return ''; |
| } |
|
|
| return [stream.stdout, stream.stderr, stream.output] |
| .map(toText) |
| .filter(Boolean) |
| .join('') |
| .trim(); |
| } |
|
|
| function nonEmptyLines(text) { |
| return toText(text) |
| .split(/\r\n|\r|\n/) |
| .map((line) => line.trim()) |
| .filter(Boolean); |
| } |
|
|
| function firstDiagnostic(text) { |
| return nonEmptyLines(text).slice(0, 6); |
| } |
|
|
| function stripForComplexityScan(source, language) { |
| let text = toText(source); |
|
|
| text = text.replace(/\/\*[\s\S]*?\*\//g, ' '); |
|
|
| if (language === 'python') { |
| text = text.replace(/#.*$/gm, ' '); |
| } else { |
| text = text.replace(/\/\/.*$/gm, ' '); |
| } |
|
|
| return text |
| .replace(/"""[\s\S]*?"""/g, '""') |
| .replace(/'''[\s\S]*?'''/g, "''") |
| .replace(/`(?:\\.|[^`])*`/g, '""') |
| .replace(/"(?:\\.|[^"\\])*"/g, '""') |
| .replace(/'(?:\\.|[^'\\])*'/g, "''"); |
| } |
|
|
| function estimatePythonLoopDepth(source) { |
| const lines = source.split(/\r\n|\r|\n/); |
| const loopIndents = []; |
| let maxDepth = 0; |
| let hasLoop = false; |
|
|
| for (const line of lines) { |
| if (!line.trim()) { |
| continue; |
| } |
|
|
| const indent = line.match(/^\s*/)?.[0].replace(/\t/g, ' ').length ?? 0; |
|
|
| while (loopIndents.length && indent <= loopIndents[loopIndents.length - 1]) { |
| loopIndents.pop(); |
| } |
|
|
| if (/^\s*(for|while)\b/.test(line)) { |
| hasLoop = true; |
| loopIndents.push(indent); |
| maxDepth = Math.max(maxDepth, loopIndents.length); |
| } |
| } |
|
|
| return { maxDepth, hasLoop }; |
| } |
|
|
| function estimateBraceLoopDepth(source) { |
| const lines = source.split(/\r\n|\r|\n/); |
| let braceDepth = 0; |
| const loopBlockDepths = []; |
| let maxDepth = 0; |
| let hasLoop = false; |
|
|
| for (const line of lines) { |
| const trimmed = line.trim(); |
|
|
| while (loopBlockDepths.length && braceDepth < loopBlockDepths[loopBlockDepths.length - 1]) { |
| loopBlockDepths.pop(); |
| } |
|
|
| const loopMatches = trimmed.match(/\b(for|while)\s*\(/g) ?? []; |
|
|
| if (loopMatches.length > 0) { |
| hasLoop = true; |
| maxDepth = Math.max(maxDepth, loopBlockDepths.length + loopMatches.length); |
| } |
|
|
| const opens = (line.match(/\{/g) ?? []).length; |
| const closes = (line.match(/\}/g) ?? []).length; |
| const loopBlocksOnLine = Math.min(loopMatches.length, opens); |
|
|
| for (let index = 0; index < loopBlocksOnLine; index += 1) { |
| loopBlockDepths.push(braceDepth + index + 1); |
| } |
|
|
| braceDepth = Math.max(0, braceDepth + opens - closes); |
|
|
| while (loopBlockDepths.length && braceDepth < loopBlockDepths[loopBlockDepths.length - 1]) { |
| loopBlockDepths.pop(); |
| } |
| } |
|
|
| return { maxDepth, hasLoop }; |
| } |
|
|
| function hasLogarithmicLoopHint(source) { |
| const patterns = [ |
| /\b\w+\s*(\*=|\/=|<<=|>>=)\s*2\b/, |
| /\b\w+\s*=\s*\w+\s*([*/]|<<|>>)\s*2\b/, |
| /\b\w+\s*=\s*\w+\s*\/\s*2\b/, |
| /\bmid\s*=/i, |
| /\bbinary\s*search\b/i, |
| ]; |
|
|
| return patterns.some((pattern) => pattern.test(source)); |
| } |
|
|
| function countHeapOperations(source) { |
| const pythonHeapOps = source.match(/\b(?:heapq\.)?(?:heappush|heappop|heapreplace|heappushpop|heapify)\s*\(/g)?.length ?? 0; |
| const namedHeapOps = source.match(/\b(?:heap|pq|queue|priorityQueue|minHeap|maxHeap)\.(?:push|pop|offer|poll|add|remove|enqueue|dequeue)\s*\(/g)?.length ?? 0; |
| const cppPriorityQueueOps = /\bpriority_queue\s*</.test(source) |
| ? source.match(/\.(?:push|pop|emplace)\s*\(/g)?.length ?? 0 |
| : 0; |
| const javaPriorityQueueOps = /\bPriorityQueue\s*</.test(source) |
| ? source.match(/\.(?:offer|poll|add|remove)\s*\(/g)?.length ?? 0 |
| : 0; |
|
|
| return pythonHeapOps + namedHeapOps + cppPriorityQueueOps + javaPriorityQueueOps; |
| } |
|
|
| function hasKBoundedHeapSignal(source) { |
| return /\bk\b|merge\s+k|k\s+sorted|lists?\s*\.length|lists?\s*\.size\s*\(|len\s*\(\s*lists?\s*\)|enumerate\s*\(\s*lists?\s*\)|for\s+\w+\s+in\s+lists?\b/i.test(source); |
| } |
|
|
| function estimateComplexity(code, runtime) { |
| const source = stripForComplexityScan(code, runtime.pistonLanguage); |
| const loopInfo = |
| runtime.pistonLanguage === 'python' |
| ? estimatePythonLoopDepth(source) |
| : estimateBraceLoopDepth(source); |
| const logHint = hasLogarithmicLoopHint(source); |
| const heapOperationCount = countHeapOperations(source); |
| const heapDetected = heapOperationCount > 0 || /\bheapq\b|\bPriorityQueue\b|\bpriority_queue\s*</.test(source); |
| const heapWorkDetected = heapOperationCount > 0; |
| const kBoundedHeap = heapDetected && hasKBoundedHeapSignal(source); |
| let timeComplexity = 'O(1)'; |
| let summary = 'No loops were detected, so this looks constant for the scanned code path.'; |
| const evidence = []; |
|
|
| if (loopInfo.hasLoop) { |
| evidence.push(`Detected loop nesting depth: ${loopInfo.maxDepth || 1}.`); |
| } else { |
| evidence.push('No for/while loops detected.'); |
| } |
|
|
| if (logHint) { |
| evidence.push('Detected halving/doubling or binary-search style loop update.'); |
| } |
|
|
| if (heapDetected) { |
| evidence.push(`Detected ${Math.max(heapOperationCount, 1)} heap/priority-queue operation${heapOperationCount === 1 ? '' : 's'}.`); |
| } |
|
|
| if (kBoundedHeap) { |
| evidence.push('Heap size appears bounded by k input lists.'); |
| } |
|
|
| if (loopInfo.hasLoop && heapWorkDetected) { |
| timeComplexity = kBoundedHeap ? 'O(N log k)' : 'O(n log n)'; |
| summary = kBoundedHeap |
| ? 'Heap operations are performed while merging k input lists, so each of the N output elements costs log k.' |
| : 'Heap operations inside traversal add a logarithmic factor to the repeated work.'; |
| } else if (loopInfo.maxDepth >= 2) { |
| timeComplexity = 'O(n^2)'; |
| summary = 'Nested loops were detected, so the highlighted curve is quadratic.'; |
| } else if (loopInfo.hasLoop && logHint) { |
| timeComplexity = 'O(log n)'; |
| summary = 'A single loop with halving/doubling behavior was detected, so the highlighted curve is logarithmic.'; |
| } else if (loopInfo.hasLoop) { |
| timeComplexity = 'O(n)'; |
| summary = 'A single loop was detected, so the highlighted curve is linear.'; |
| } |
|
|
| return { |
| timeComplexity, |
| source: 'static_code_scan', |
| confidence: heapWorkDetected || loopInfo.maxDepth >= 2 || (loopInfo.hasLoop && !logHint) ? 'medium' : 'low', |
| summary, |
| evidence, |
| }; |
| } |
|
|
| function isTimeoutSignal(signal) { |
| return ['SIGKILL', 'SIGTERM', 'SIGXCPU'].includes(String(signal ?? '').toUpperCase()); |
| } |
|
|
| function looksLikeTimeout(text) { |
| return /timeout|timed out|time limit|killed/i.test(text); |
| } |
|
|
| function normalizePistonApiError(payload, fallback) { |
| if (!payload) { |
| return fallback; |
| } |
|
|
| if (typeof payload === 'string') { |
| return payload; |
| } |
|
|
| if (typeof payload.message === 'string') { |
| return payload.message; |
| } |
|
|
| if (typeof payload.error === 'string') { |
| return payload.error; |
| } |
|
|
| return fallback; |
| } |
|
|
| function pistonExecuteUrl() { |
| return (process.env.PISTON_EXECUTE_URL || DEFAULT_PISTON_EXECUTE_URL).trim(); |
| } |
|
|
| function pistonHeaders() { |
| const headers = { |
| 'Content-Type': 'application/json', |
| }; |
| const authorization = |
| process.env.PISTON_AUTHORIZATION || |
| process.env.PISTON_API_KEY || |
| process.env.PISTON_TOKEN || |
| ''; |
|
|
| if (authorization.trim()) { |
| headers.Authorization = authorization.trim(); |
| } |
|
|
| return headers; |
| } |
|
|
| function buildCompilerAnalysis({ |
| runtime, |
| code, |
| phase, |
| status, |
| exitCode = null, |
| signal = null, |
| stdout = '', |
| stderr = '', |
| diagnostics = '', |
| summary, |
| }) { |
| return { |
| source: 'compiler', |
| language: runtime.pistonLanguage, |
| version: runtime.version, |
| phase, |
| status, |
| exitCode, |
| signal, |
| stdoutLines: nonEmptyLines(stdout).length, |
| stderrLines: nonEmptyLines(stderr).length, |
| summary, |
| details: firstDiagnostic(diagnostics || stderr || stdout), |
| complexity: estimateComplexity(code, runtime), |
| }; |
| } |
|
|
| function classifyPistonResult(result, executionTime, runtime, code) { |
| const compile = result?.compile; |
| const run = result?.run; |
| const compileCode = typeof compile?.code === 'number' ? compile.code : 0; |
| const runCode = typeof run?.code === 'number' ? run.code : 0; |
| const compileText = combinedStream(compile); |
| const runStdout = toText(run?.stdout); |
| const runStderr = toText(run?.stderr); |
| const runOutput = toText(run?.output) || runStdout; |
| const runText = [runStderr, runOutput].filter(Boolean).join('\n'); |
|
|
| if (compile && compileCode !== 0) { |
| return { |
| output: toText(compile.stdout), |
| error: compileText || 'Compilation failed.', |
| executionTime, |
| status: 'compile_error', |
| analysis: buildCompilerAnalysis({ |
| runtime, |
| code, |
| phase: 'compile', |
| status: 'compile_error', |
| exitCode: compileCode, |
| signal: compile.signal ?? null, |
| stdout: compile?.stdout, |
| stderr: compile?.stderr, |
| diagnostics: compileText, |
| summary: 'The compiler rejected the source before execution.', |
| }), |
| }; |
| } |
|
|
| if (run && (isTimeoutSignal(run.signal) || looksLikeTimeout(runText))) { |
| return { |
| output: runStdout, |
| error: runStderr || 'Execution timed out.', |
| executionTime, |
| status: 'timeout', |
| analysis: buildCompilerAnalysis({ |
| runtime, |
| code, |
| phase: 'run', |
| status: 'timeout', |
| exitCode: runCode, |
| signal: run.signal ?? null, |
| stdout: runStdout, |
| stderr: runStderr, |
| diagnostics: runText, |
| summary: 'The runtime stopped the program because it exceeded the execution limit.', |
| }), |
| }; |
| } |
|
|
| if (run && runCode !== 0) { |
| return { |
| output: runStdout, |
| error: runStderr || runOutput || `Runtime error. Process exited with code ${runCode}.`, |
| executionTime, |
| status: 'runtime_error', |
| analysis: buildCompilerAnalysis({ |
| runtime, |
| code, |
| phase: 'run', |
| status: 'runtime_error', |
| exitCode: runCode, |
| signal: run.signal ?? null, |
| stdout: runStdout, |
| stderr: runStderr, |
| diagnostics: runText, |
| summary: `The program started, then exited with code ${runCode}.`, |
| }), |
| }; |
| } |
|
|
| return { |
| output: runStdout || runOutput, |
| error: runStderr, |
| executionTime, |
| status: 'success', |
| analysis: buildCompilerAnalysis({ |
| runtime, |
| code, |
| phase: run ? 'run' : 'compile', |
| status: 'success', |
| exitCode: runCode, |
| signal: run?.signal ?? null, |
| stdout: runStdout || runOutput, |
| stderr: runStderr, |
| diagnostics: runStderr, |
| summary: runStdout || runOutput |
| ? 'The compiler/runtime completed successfully and produced stdout.' |
| : 'The compiler/runtime completed successfully with no stdout.', |
| }), |
| }; |
| } |
|
|
| async function readResponsePayload(response) { |
| const text = await response.text(); |
|
|
| if (!text) { |
| return null; |
| } |
|
|
| try { |
| return JSON.parse(text); |
| } catch { |
| return text; |
| } |
| } |
|
|
| export async function executeCode({ code, language, input = '' }) { |
| const runtime = getRuntime(language); |
| const startedAt = Date.now(); |
|
|
| if (!runtime) { |
| throw new CompilerError( |
| `Unsupported language: ${language}. Supported languages are ${getSupportedLanguages().join(', ')}.`, |
| 'unsupported_language', |
| 400, |
| ); |
| } |
|
|
| if (typeof code !== 'string' || code.trim().length === 0) { |
| throw new CompilerError('Code is required.', 'validation_error', 400); |
| } |
|
|
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); |
|
|
| try { |
| const response = await fetch(pistonExecuteUrl(), { |
| method: 'POST', |
| headers: pistonHeaders(), |
| signal: controller.signal, |
| body: JSON.stringify({ |
| language: runtime.pistonLanguage, |
| version: runtime.version, |
| files: [ |
| { |
| name: runtime.fileName, |
| content: code, |
| }, |
| ], |
| stdin: typeof input === 'string' ? input : String(input ?? ''), |
| compile_timeout: COMPILE_TIMEOUT_MS, |
| run_timeout: RUN_TIMEOUT_MS, |
| }), |
| }); |
|
|
| const executionTime = Date.now() - startedAt; |
|
|
| if (!response.ok) { |
| const payload = await readResponsePayload(response); |
| const message = normalizePistonApiError(payload, `Piston API returned HTTP ${response.status}.`); |
| const status = response.status === 408 || response.status === 504 ? 'timeout' : 'piston_error'; |
| throw new CompilerError(message, status, response.status === 408 ? 408 : 502, executionTime); |
| } |
|
|
| const result = await response.json(); |
| return classifyPistonResult(result, executionTime, runtime, code); |
| } catch (error) { |
| const executionTime = Date.now() - startedAt; |
|
|
| if (error instanceof CompilerError) { |
| throw error; |
| } |
|
|
| if (error?.name === 'AbortError') { |
| throw new CompilerError('Execution timed out before Piston responded.', 'timeout', 504, executionTime); |
| } |
|
|
| throw new CompilerError( |
| `Unable to reach Piston execution service: ${error?.message ?? 'network error'}`, |
| 'piston_unavailable', |
| 502, |
| executionTime, |
| ); |
| } finally { |
| clearTimeout(timeoutId); |
| } |
| } |
|
|