diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile index a73990b..a1ad093 100644 --- a/apps/backend/Dockerfile +++ b/apps/backend/Dockerfile @@ -1,4 +1,21 @@ FROM docker.io/cloudflare/sandbox:0.6.0-python +# Install C++ compiler and tools for C++ language support +RUN apt-get update && apt-get install -y \ + g++ \ + build-essential \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install nlohmann/json library (header-only JSON library for C++) +RUN mkdir -p /usr/local/include && \ + cd /tmp && \ + git clone --depth 1 https://github.com/nlohmann/json.git && \ + cp json/single_include/nlohmann/json.hpp /usr/local/include/ && \ + rm -rf /tmp/json + +# Verify C++ compiler installation +RUN g++ --version + # Required during local development to access exposed ports EXPOSE 8080 \ No newline at end of file diff --git a/apps/backend/src/problem-actions/src/code-generator/cpp-generator.ts b/apps/backend/src/problem-actions/src/code-generator/cpp-generator.ts new file mode 100644 index 0000000..c9697b9 --- /dev/null +++ b/apps/backend/src/problem-actions/src/code-generator/cpp-generator.ts @@ -0,0 +1,186 @@ +import type { FunctionSignatureSchema, TypeDef } from "@repo/api-types"; +import type { CodeGenerator } from "./types"; + +export class CppGenerator implements CodeGenerator { + private includes = new Set(); + + typeToString(typeDef: TypeDef): string { + switch (typeDef.kind) { + case "primitive": { + const map: Record = { + int: "long long", + float: "double", + string: "std::string", + boolean: "bool", + null: "std::nullptr_t", + }; + return map[typeDef.type]; + } + case "array": + this.includes.add("vector"); + return `std::vector<${this.typeToString(typeDef.items)}>`; + case "object": + // Inline objects become std::map + // For heterogeneous objects, we'd need a struct, but for now use generic map + this.includes.add("map"); + this.includes.add("string"); + return "std::map"; + case "map": + this.includes.add("unordered_map"); + return `std::unordered_map<${this.typeToString(typeDef.keyType)}, ${this.typeToString(typeDef.valueType)}>`; + case "tuple": + this.includes.add("tuple"); + return `std::tuple<${typeDef.items.map((i) => this.typeToString(i)).join(", ")}>`; + case "union": { + // Use std::variant for unions + this.includes.add("variant"); + return `std::variant<${typeDef.types.map((t) => this.typeToString(t)).join(", ")}>`; + } + case "reference": + // Named types (TreeNode, ListNode, etc.) - use pointer notation + return `${typeDef.name}*`; + } + } + + generateTypeDefinitions(schema: FunctionSignatureSchema): string { + if (!schema.namedTypes?.length) return ""; + + return schema.namedTypes + .map((nt) => { + if (nt.definition.kind === "object") { + const props = Object.entries(nt.definition.properties) + .map(([k, v]) => ` ${this.typeToString(v)} ${k};`) + .join("\n"); + // Generate constructor + const constructorParams = Object.entries(nt.definition.properties) + .map(([k, v]) => `${this.typeToString(v)} _${k}`) + .join(", "); + const constructorInits = Object.keys(nt.definition.properties) + .map((k) => `${k}(_${k})`) + .join(", "); + + return `struct ${nt.name} {\n${props}\n ${nt.name}(${constructorParams}) : ${constructorInits} {}\n};`; + } + return `using ${nt.name} = ${this.typeToString(nt.definition)};`; + }) + .join("\n\n"); + } + + generateScaffold(schema: FunctionSignatureSchema): string { + const params = schema.parameters + .map((p) => { + const typeStr = this.typeToString(p.type); + return `${typeStr} ${p.name}`; + }) + .join(", "); + const returnType = this.typeToString(schema.returnType); + return `${returnType} runSolution(${params}) {\n // TODO: implement your solution here\n throw std::runtime_error("Not implemented");\n}`; + } + + generateStarterCode(schema: FunctionSignatureSchema): string { + // Clear includes before generating to get fresh set + this.includes.clear(); + + // Generate type definitions first (this populates includes) + const typeDefs = this.generateTypeDefinitions(schema); + + // Generate scaffold (this also populates includes) + const scaffold = this.generateScaffold(schema); + + // Build include statements + const commonIncludes = ["iostream", "stdexcept"]; + const allIncludes = [...new Set([...commonIncludes, ...Array.from(this.includes)])].sort(); + const includeLines = allIncludes.map((inc) => `#include <${inc}>`).join("\n"); + + // Combine all parts + const parts = [includeLines]; + if (typeDefs) { + parts.push("\n\n" + typeDefs); + } + parts.push("\n\n" + scaffold); + + return parts.join(""); + } + + /** + * Generate the runner code that calls runSolution with deserialized parameters + */ + generateRunnerCode(schema: FunctionSignatureSchema): string { + // Generate parameter deserialization code + const paramDeserializations = schema.parameters + .map( + (p, index) => + ` auto ${p.name} = input[${index}].get<${this.typeToString(p.type)}>();` + ) + .join("\n"); + + const paramNames = schema.parameters.map((p) => p.name).join(", "); + + return ` +#include +#include +#include + +using json = nlohmann::json; + +int main(int argc, char* argv[]) { + if (argc < 3) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::string inputPath = argv[1]; + std::string outputPath = argv[2]; + + try { + // Read input JSON + std::ifstream inputFile(inputPath); + if (!inputFile.is_open()) { + throw std::runtime_error("Failed to open input file"); + } + json input = json::parse(inputFile); + inputFile.close(); + + // Capture stdout + std::stringstream stdoutCapture; + std::streambuf* oldCout = std::cout.rdbuf(stdoutCapture.rdbuf()); + + // Deserialize parameters +${paramDeserializations} + + // Call user's solution + auto result = runSolution(${paramNames}); + + // Restore stdout + std::cout.rdbuf(oldCout); + + // Write output JSON + json output; + output["success"] = true; + output["result"] = result; + output["stdout"] = stdoutCapture.str(); + + std::ofstream outputFile(outputPath); + outputFile << output.dump(); + outputFile.close(); + + } catch (const std::exception& e) { + json output; + output["success"] = false; + output["error"] = e.what(); + output["trace"] = ""; + output["stdout"] = ""; + + std::ofstream outputFile(outputPath); + outputFile << output.dump(); + outputFile.close(); + + // Exit with code 0 so main code can read output.json and handle the error + return 0; + } + + return 0; +} +`.trim(); + } +} diff --git a/apps/backend/src/problem-actions/src/code-generator/index.ts b/apps/backend/src/problem-actions/src/code-generator/index.ts index 01cf33a..445784d 100644 --- a/apps/backend/src/problem-actions/src/code-generator/index.ts +++ b/apps/backend/src/problem-actions/src/code-generator/index.ts @@ -1,5 +1,6 @@ import { TypeScriptGenerator } from "./typescript-generator"; import { PythonGenerator } from "./python-generator"; +import { CppGenerator } from "./cpp-generator"; import type { CodeGenerator, CodeGenLanguage } from "./types"; export function createCodeGenerator(language: CodeGenLanguage): CodeGenerator { @@ -8,6 +9,8 @@ export function createCodeGenerator(language: CodeGenLanguage): CodeGenerator { return new TypeScriptGenerator(); case "python": return new PythonGenerator(); + case "cpp": + return new CppGenerator(); default: throw new Error(`Unsupported language: ${language}`); } @@ -18,3 +21,4 @@ export type { CodeGenerator, CodeGenLanguage } from "./types"; export { CodeGenLanguageSchema } from "./types"; export { TypeScriptGenerator } from "./typescript-generator"; export { PythonGenerator } from "./python-generator"; +export { CppGenerator } from "./cpp-generator"; diff --git a/apps/backend/src/problem-actions/src/code-generator/types.ts b/apps/backend/src/problem-actions/src/code-generator/types.ts index 7d2f23f..73c6125 100644 --- a/apps/backend/src/problem-actions/src/code-generator/types.ts +++ b/apps/backend/src/problem-actions/src/code-generator/types.ts @@ -16,5 +16,5 @@ export interface CodeGenerator { } // Languages supported by the code generator (subset of all supported languages) -export const CodeGenLanguageSchema = z.enum(["typescript", "python"]); +export const CodeGenLanguageSchema = z.enum(["typescript", "python", "cpp"]); export type CodeGenLanguage = z.infer; diff --git a/apps/backend/src/problem-actions/src/run-user-solution.ts b/apps/backend/src/problem-actions/src/run-user-solution.ts index afb73ad..9543bff 100644 --- a/apps/backend/src/problem-actions/src/run-user-solution.ts +++ b/apps/backend/src/problem-actions/src/run-user-solution.ts @@ -16,7 +16,7 @@ export async function runUserSolution( language: SupportedLanguage = "typescript", env: Env, ): Promise { - const { testCases, generatedByUserId } = await getProblem(problemId, db); + const { testCases, generatedByUserId, functionSignatureSchema } = await getProblem(problemId, db); if (!testCases || testCases.length === 0) { throw new Error( "No test cases found. Please generate test case descriptions and inputs first.", @@ -41,7 +41,19 @@ export async function runUserSolution( } const config = getLanguageConfig(language); - const runnerTemplate = getRunnerTemplate(language); + + // For C++, generate runner dynamically; for others, use static template + let runnerTemplate: string; + if (language === "cpp") { + if (!functionSignatureSchema) { + throw new Error("Function signature schema is required for C++ execution"); + } + const { CppGenerator } = await import("./code-generator/cpp-generator"); + const generator = new CppGenerator(); + runnerTemplate = generator.generateRunnerCode(functionSignatureSchema); + } else { + runnerTemplate = getRunnerTemplate(language); + } const solutionPath = `${WORK_DIR}/solution.${config.extension}`; const runnerPath = `${WORK_DIR}/runner.${config.extension}`; @@ -56,6 +68,36 @@ export async function runUserSolution( await sandbox.uploadFile(Buffer.from(preparedCode, "utf-8"), solutionPath); await sandbox.uploadFile(Buffer.from(runnerTemplate, "utf-8"), runnerPath); + // For C++, compile the code first + if (language === "cpp") { + // Combine solution and runner into single file for compilation + const combinedCode = `${preparedCode}\n\n${runnerTemplate}`; + const combinedPath = `${WORK_DIR}/combined.cpp`; + await sandbox.uploadFile(Buffer.from(combinedCode, "utf-8"), combinedPath); + + // Compile C++ code with 30 second timeout + const compileCommand = `g++ -std=c++17 -O2 -Wall -Wextra -I/usr/local/include ${combinedPath} -o ${WORK_DIR}/runner`; + const compileResult = await sandbox.executeCommand( + compileCommand, + WORK_DIR, + 30000, // 30 second timeout for compilation + ); + + // Check for compilation errors + if (compileResult.exitCode !== 0) { + const compilationError = compileResult.stderr || "Compilation failed"; + // Return compilation error for all test cases + results = testCases.map((testCase) => ({ + testCase, + status: "error" as const, + actual: null, + expected: testCase.expected, + error: `Compilation Error:\n${compilationError}`, + })); + return results; + } + } + const limit = pLimit(getConcurrency(env)); const settledResults = await Promise.allSettled( testCases.map((testCase, index) => @@ -72,19 +114,40 @@ export async function runUserSolution( await sandbox.uploadFile(Buffer.from(inputJson, "utf-8"), inputPath); // Execute the runner with unique input/output paths - const command = `${config.runCommand} runner.${config.extension} ${inputPath} ${outputPath}`; - const result = await sandbox.executeCommand(command, WORK_DIR); + // For C++, run the compiled binary; for others, use the interpreter/runtime + const command = + language === "cpp" + ? `${WORK_DIR}/runner ${inputPath} ${outputPath}` + : `${config.runCommand} runner.${config.extension} ${inputPath} ${outputPath}`; + const result = await sandbox.executeCommand( + command, + WORK_DIR, + 10000, // 10 second timeout per test case + ); console.log("result", JSON.stringify(result, null, 2)); // If exitCode !== 0, treat as runner execution failure if (result.exitCode !== 0) { + // Provide C++-specific error messages for common exit codes + let errorMessage = + "Execution failed. Please abide by the given function signature and structure."; + if (language === "cpp") { + if (result.exitCode === 139) { + errorMessage = "Segmentation Fault (exit code 139): Your program tried to access invalid memory. Check for null pointer dereferences, array out of bounds, or stack overflow."; + } else if (result.exitCode === 134) { + errorMessage = "Aborted (exit code 134): Your program was terminated, possibly due to an assertion failure or abort() call."; + } else if (result.exitCode === 136) { + errorMessage = "Floating Point Exception (exit code 136): Division by zero or invalid arithmetic operation."; + } else if (result.stderr) { + errorMessage = `Runtime Error:\n${result.stderr}`; + } + } return { testCase, status: "error", actual: null, expected: testCase.expected, - error: - "Execution failed. Please abide by the given function signature and structure.", + error: errorMessage, }; } @@ -259,7 +322,16 @@ export async function runUserSolutionWithCustomInputs( } const config = getLanguageConfig(language); - const runnerTemplate = getRunnerTemplate(language); + + // For C++, generate runner dynamically; for others, use static template + let runnerTemplate: string; + if (language === "cpp") { + const { CppGenerator } = await import("./code-generator/cpp-generator"); + const generator = new CppGenerator(); + runnerTemplate = generator.generateRunnerCode(schema); + } else { + runnerTemplate = getRunnerTemplate(language); + } const userSolutionPath = `${WORK_DIR}/user.${config.extension}`; const solutionPath = `${WORK_DIR}/solution.${config.extension}`; @@ -281,6 +353,35 @@ export async function runUserSolutionWithCustomInputs( WORK_DIR, ); + // For C++, compile the code first + if (language === "cpp") { + // Combine solution and runner into single file for compilation + const combinedCode = `${preparedUserCode}\n\n${runnerTemplate}`; + const combinedPath = `${WORK_DIR}/combined.cpp`; + await sandbox.uploadFile(Buffer.from(combinedCode, "utf-8"), combinedPath); + + // Compile C++ code with 30 second timeout + const compileCommand = `g++ -std=c++17 -O2 -Wall -Wextra -I/usr/local/include ${combinedPath} -o ${WORK_DIR}/runner`; + const compileResult = await sandbox.executeCommand( + compileCommand, + WORK_DIR, + 30000, // 30 second timeout for compilation + ); + + // Check for compilation errors + if (compileResult.exitCode !== 0) { + const compilationError = compileResult.stderr || "Compilation failed"; + // Return compilation error for all custom inputs + const results: CustomTestResult[] = customInputs.map((input) => ({ + input, + expected: null, + actual: null, + error: `Compilation Error:\n${compilationError}`, + })); + return results; + } + } + const limit = pLimit(getConcurrency(env)); const settledResults = await Promise.allSettled( customInputs.map((input, index) => @@ -307,18 +408,39 @@ export async function runUserSolutionWithCustomInputs( ); // Run user solution - const command = `${config.runCommand} runner.${config.extension} ${inputPath} ${outputPath}`; - const result = await sandbox.executeCommand(command, WORK_DIR); + // For C++, run the compiled binary; for others, use the interpreter/runtime + const command = + language === "cpp" + ? `${WORK_DIR}/runner ${inputPath} ${outputPath}` + : `${config.runCommand} runner.${config.extension} ${inputPath} ${outputPath}`; + const result = await sandbox.executeCommand( + command, + WORK_DIR, + 10000, // 10 second timeout per test case + ); console.log("result", JSON.stringify(result, null, 2)); // If exitCode !== 0, treat as runner execution failure if (result.exitCode !== 0) { + // Provide C++-specific error messages for common exit codes + let errorMessage = + "Execution failed. Please abide by the given function signature and structure."; + if (language === "cpp") { + if (result.exitCode === 139) { + errorMessage = "Segmentation Fault (exit code 139): Your program tried to access invalid memory. Check for null pointer dereferences, array out of bounds, or stack overflow."; + } else if (result.exitCode === 134) { + errorMessage = "Aborted (exit code 134): Your program was terminated, possibly due to an assertion failure or abort() call."; + } else if (result.exitCode === 136) { + errorMessage = "Floating Point Exception (exit code 136): Division by zero or invalid arithmetic operation."; + } else if (result.stderr) { + errorMessage = `Runtime Error:\n${result.stderr}`; + } + } return { input, expected, actual: null, - error: - "Execution failed. Please abide by the given function signature and structure.", + error: errorMessage, }; } diff --git a/apps/backend/src/problem-actions/src/runners.ts b/apps/backend/src/problem-actions/src/runners.ts index da06efe..8ad54d3 100644 --- a/apps/backend/src/problem-actions/src/runners.ts +++ b/apps/backend/src/problem-actions/src/runners.ts @@ -128,6 +128,73 @@ except Exception as e: # Exit with code 0 so main code can read output.json and handle the error `.trim(); +// C++ runner template +// This template will be combined with user solution and compiled +// Args: ./runner +export const CPP_RUNNER = ` +#include +#include +#include + +using json = nlohmann::json; + +int main(int argc, char* argv[]) { + if (argc < 3) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::string inputPath = argv[1]; + std::string outputPath = argv[2]; + + try { + // Read input JSON + std::ifstream inputFile(inputPath); + if (!inputFile.is_open()) { + throw std::runtime_error("Failed to open input file"); + } + json input = json::parse(inputFile); + inputFile.close(); + + // Capture stdout + std::stringstream stdoutCapture; + std::streambuf* oldCout = std::cout.rdbuf(stdoutCapture.rdbuf()); + + // Call user's solution - this will be customized per function signature + auto result = runSolution(input); + + // Restore stdout + std::cout.rdbuf(oldCout); + + // Write output JSON + json output; + output["success"] = true; + output["result"] = result; + output["stdout"] = stdoutCapture.str(); + + std::ofstream outputFile(outputPath); + outputFile << output.dump(); + outputFile.close(); + + } catch (const std::exception& e) { + json output; + output["success"] = false; + output["error"] = e.what(); + output["trace"] = ""; + output["stdout"] = ""; + + std::ofstream outputFile(outputPath); + outputFile << output.dump(); + outputFile.close(); + + // Exit with code 0 so main code can read output.json and handle the error + return 0; + } + + return 0; +} +`.trim(); + /** * Prepares TypeScript/JavaScript code by ensuring it exports runSolution */ @@ -147,6 +214,13 @@ function preparePythonCode(userCode: string): string { return userCode.trim(); } +/** + * Prepares C++ code (no changes needed - C++ doesn't use exports) + */ +function prepareCppCode(userCode: string): string { + return userCode.trim(); +} + export const LANGUAGE_CONFIGS: Record = { typescript: { extension: "ts", @@ -166,12 +240,19 @@ export const LANGUAGE_CONFIGS: Record = { sandboxLanguage: "python", prepareCode: preparePythonCode, }, + cpp: { + extension: "cpp", + runCommand: "g++", + sandboxLanguage: "cpp", + prepareCode: prepareCppCode, + }, }; export const RUNNER_TEMPLATES: Record = { typescript: TS_RUNNER, javascript: JS_RUNNER, python: PY_RUNNER, + cpp: CPP_RUNNER, }; export function getRunnerTemplate(language: SupportedLanguage): string { diff --git a/apps/backend/src/problem-actions/src/types.ts b/apps/backend/src/problem-actions/src/types.ts index 6a56f8c..0bb8a9e 100644 --- a/apps/backend/src/problem-actions/src/types.ts +++ b/apps/backend/src/problem-actions/src/types.ts @@ -23,7 +23,7 @@ export interface SandboxConfig { apiKey: string; } -export type SupportedLanguage = "typescript" | "javascript" | "python"; +export type SupportedLanguage = "typescript" | "javascript" | "python" | "cpp"; export interface LanguageConfig { extension: string; diff --git a/apps/web/actions/run-user-solution.ts b/apps/web/actions/run-user-solution.ts index ae7ebd3..c3868da 100644 --- a/apps/web/actions/run-user-solution.ts +++ b/apps/web/actions/run-user-solution.ts @@ -5,7 +5,7 @@ import type { TestResult, CustomTestResult } from "@repo/api-types"; export type { TestCase, TestResult, CustomTestResult } from "@repo/api-types"; // Define CodeGenLanguage type (shared with hooks) -export type CodeGenLanguage = "typescript" | "python"; +export type CodeGenLanguage = "typescript" | "python" | "cpp"; export async function runUserSolution( problemId: string, diff --git a/apps/web/app/problem/[problemId]/components/problem-render.tsx b/apps/web/app/problem/[problemId]/components/problem-render.tsx index 139e6e9..f644a4a 100644 --- a/apps/web/app/problem/[problemId]/components/problem-render.tsx +++ b/apps/web/app/problem/[problemId]/components/problem-render.tsx @@ -410,6 +410,7 @@ export default function ProblemRender({ TypeScript Python + C++ {isStarterCodeLoading && ( diff --git a/packages/api-types/src/schemas/problems.ts b/packages/api-types/src/schemas/problems.ts index e9fbccf..10039f4 100644 --- a/packages/api-types/src/schemas/problems.ts +++ b/packages/api-types/src/schemas/problems.ts @@ -87,7 +87,7 @@ export const RunSolutionRequestSchema = z example: "def solution(n: int) -> int:\n return n * 2", }), language: z - .enum(["typescript", "python"]) + .enum(["typescript", "python", "cpp"]) .optional() .default("typescript") .openapi({ @@ -112,7 +112,7 @@ export const RunCustomTestsRequestSchema = z "Array of test inputs. Each input is an array of function arguments.", }), language: z - .enum(["typescript", "python"]) + .enum(["typescript", "python", "cpp"]) .optional() .default("typescript") .openapi({ @@ -181,7 +181,7 @@ export const ProblemFocusAreasResponseSchema = z // Starter code request/response export const StarterCodeRequestSchema = z .object({ - language: z.enum(["typescript", "python"]).openapi({ + language: z.enum(["typescript", "python", "cpp"]).openapi({ example: "typescript", description: "The programming language for the starter code", }), @@ -191,7 +191,7 @@ export const StarterCodeRequestSchema = z export const StarterCodeResponseSchema = z .object({ starterCode: z.string(), - language: z.enum(["typescript", "python"]), + language: z.enum(["typescript", "python", "cpp"]), }) .openapi("StarterCodeResponse");