-
Notifications
You must be signed in to change notification settings - Fork 12
feat: Add C++ language support #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
@srivatsan0611 is attempting to deploy a commit to the anirudhkamath's projects Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive C++ language support to ClankerLoop, enabling users to solve coding problems in C++ alongside the existing TypeScript and Python options. The implementation follows a similar pattern to the existing language generators but introduces dynamic runner generation and a two-phase execution model (compilation followed by test execution).
Key Changes:
- Added C++ as a third supported language with complete end-to-end integration from frontend to backend
- Implemented
CppGeneratorclass with dynamic runner code generation based on function signatures - Created two-phase execution model: compile once (30s timeout), then run tests in parallel (10s timeout each)
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
packages/api-types/src/schemas/problems.ts |
Updated all language enum schemas to include "cpp" option |
apps/web/app/problem/[problemId]/components/problem-render.tsx |
Added C++ as a selectable language in the UI dropdown |
apps/web/actions/run-user-solution.ts |
Extended CodeGenLanguage type to include "cpp" |
apps/backend/src/problem-actions/src/types.ts |
Added "cpp" to SupportedLanguage type definition |
apps/backend/src/problem-actions/src/runners.ts |
Added C++ language config and runner template (note: template is unused) |
apps/backend/src/problem-actions/src/run-user-solution.ts |
Implemented C++ compilation logic, C++-specific error handling, and dynamic runner generation |
apps/backend/src/problem-actions/src/code-generator/types.ts |
Extended code generator schema to support "cpp" |
apps/backend/src/problem-actions/src/code-generator/index.ts |
Added C++ generator to factory function and exports |
apps/backend/src/problem-actions/src/code-generator/cpp-generator.ts |
Complete C++ code generator with type mapping, struct generation, and dynamic runner creation |
apps/backend/Dockerfile |
Installed g++ compiler and nlohmann/json library for C++ compilation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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}`; | ||
| } | ||
| } |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The C++ error handling logic (lines 131-144) is duplicated in two places: once in runUserSolution and again in runUserSolutionWithCustomInputs (lines 425-438). Consider extracting this into a shared helper function to improve maintainability and ensure consistency.
function getCppErrorMessage(exitCode: number, stderr: string): string {
if (exitCode === 139) {
return "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 (exitCode === 134) {
return "Aborted (exit code 134): Your program was terminated, possibly due to an assertion failure or abort() call.";
} else if (exitCode === 136) {
return "Floating Point Exception (exit code 136): Division by zero or invalid arithmetic operation.";
} else if (stderr) {
return `Runtime Error:\n${stderr}`;
}
return "Execution failed. Please abide by the given function signature and structure.";
}| # 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 && \ |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nlohmann/json library is installed from the latest commit on the main branch without a version pin. This could lead to unexpected behavior if the library's API changes. Consider pinning to a specific tag/release for reproducibility.
git clone --depth 1 --branch v3.11.3 https://github.com/nlohmann/json.git| git clone --depth 1 https://github.com/nlohmann/json.git && \ | |
| git clone --depth 1 --branch v3.11.3 https://github.com/nlohmann/json.git && \ |
| 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(", "); |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The generateRunnerCode method doesn't handle the case where a function has no parameters. When schema.parameters is empty, paramDeserializations will be an empty string and paramNames will be an empty string, but the generated code would still have comments and structure that suggests parameters. While this should still compile correctly, consider adding a comment or adjusting the code generation for clarity in the zero-parameter case.
| boolean: "bool", | ||
| null: "std::nullptr_t", | ||
| }; | ||
| return map[typeDef.type]; |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The typeToString method doesn't handle the case where typeDef.type doesn't match any key in the map. This would return undefined, which could cause issues downstream. Consider adding a default case or throwing an error for unsupported primitive types.
const mapped = map[typeDef.type];
if (!mapped) {
throw new Error(`Unsupported primitive type: ${typeDef.type}`);
}
return mapped;| return map[typeDef.type]; | |
| const mapped = map[typeDef.type]; | |
| if (!mapped) { | |
| throw new Error(`Unsupported primitive type: ${typeDef.type}`); | |
| } | |
| return mapped; |
| // C++ runner template | ||
| // This template will be combined with user solution and compiled | ||
| // Args: ./runner <input_path> <output_path> | ||
| export const CPP_RUNNER = ` | ||
| #include <fstream> | ||
| #include <sstream> | ||
| #include <nlohmann/json.hpp> | ||
| using json = nlohmann::json; | ||
| int main(int argc, char* argv[]) { | ||
| if (argc < 3) { | ||
| std::cerr << "Usage: " << argv[0] << " <input.json> <output.json>" << 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(); |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CPP_RUNNER template exported here is never actually used in the codebase. The C++ implementation dynamically generates runner code via CppGenerator.generateRunnerCode() instead of using this static template. Consider removing this unused export to avoid confusion about which runner template is actually used for C++.
| // 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; | ||
| } | ||
| } |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The C++ compilation logic (lines 71-99) is duplicated in two places: once in runUserSolution and again in runUserSolutionWithCustomInputs (lines 356-383). Consider extracting this into a shared helper function to improve maintainability and ensure consistency.
async function compileCppCode(
sandbox: Sandbox,
preparedCode: string,
runnerTemplate: string,
workDir: string
): Promise<{ success: boolean; error?: string }> {
const combinedCode = `${preparedCode}\n\n${runnerTemplate}`;
const combinedPath = `${workDir}/combined.cpp`;
await sandbox.uploadFile(Buffer.from(combinedCode, "utf-8"), combinedPath);
const compileCommand = `g++ -std=c++17 -O2 -Wall -Wextra -I/usr/local/include ${combinedPath} -o ${workDir}/runner`;
const compileResult = await sandbox.executeCommand(
compileCommand,
workDir,
30000,
);
if (compileResult.exitCode !== 0) {
return {
success: false,
error: compileResult.stderr || "Compilation failed"
};
}
return { success: true };
}| 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/ && \ |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The installation of nlohmann/json only copies the single header file to /usr/local/include/ directly, but the compile command uses -I/usr/local/include and includes <nlohmann/json.hpp> (note the nlohmann/ subdirectory). This path mismatch will cause compilation failures.
Fix this by creating the proper directory structure:
RUN mkdir -p /usr/local/include/nlohmann && \
cd /tmp && \
git clone --depth 1 https://github.com/nlohmann/json.git && \
cp json/single_include/nlohmann/json.hpp /usr/local/include/nlohmann/ && \
rm -rf /tmp/json| 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/ && \ | |
| RUN mkdir -p /usr/local/include/nlohmann && \ | |
| cd /tmp && \ | |
| git clone --depth 1 https://github.com/nlohmann/json.git && \ | |
| cp json/single_include/nlohmann/json.hpp /usr/local/include/nlohmann/ && \ |
| 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};`; |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generateTypeDefinitions method generates constructors for struct definitions, but it doesn't handle the case where an object has no properties (empty properties object). This would result in invalid C++ syntax with trailing colons in the constructor. Consider adding a check:
if (nt.definition.kind === "object") {
const props = Object.entries(nt.definition.properties)
.map(([k, v]) => ` ${this.typeToString(v)} ${k};`)
.join("\n");
// Only generate constructor if there are properties
if (Object.keys(nt.definition.properties).length === 0) {
return `struct ${nt.name} {\n${props}\n};`;
}
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};`;
}| generateScaffold(schema: FunctionSignatureSchema): string { | ||
| const params = schema.parameters | ||
| .map((p) => { | ||
| const typeStr = this.typeToString(p.type); |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The C++ generator doesn't handle optional parameters (unlike TypeScript and Python generators which check p.optional). If the schema includes optional parameters, they won't be properly represented in C++. Consider adding support for optional parameters using std::optional<T>:
const params = schema.parameters
.map((p) => {
const typeStr = this.typeToString(p.type);
if (p.optional) {
this.includes.add("optional");
return `std::optional<${typeStr}> ${p.name}`;
}
return `${typeStr} ${p.name}`;
})
.join(", ");| const typeStr = this.typeToString(p.type); | |
| const typeStr = this.typeToString(p.type); | |
| if (p.optional) { | |
| this.includes.add("optional"); | |
| return `std::optional<${typeStr}> ${p.name}`; | |
| } |
Summary
Adds complete C++ language support to ClankerLoop, enabling users to solve problems in C++.
Implementation Details