import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { writeFile, unlink } from 'node:fs/promises';
import { statSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { tmpdir, homedir } from 'node:os';
import { runGxc, buildLoadpathEnv } from '../gxi.js';
export function registerCompileCheckTool(server: McpServer): void {
server.registerTool(
'gerbil_compile_check',
{
title: 'Compile Check Gerbil Code',
description:
'Run the Gerbil compiler (gxc -S) on code to catch compilation errors ' +
'such as unbound identifiers and type issues that syntax checking alone misses. ' +
'Validates the full Gerbil compilation pipeline without producing C output. ' +
'Provide either code as a string or a file_path to an existing .ss file.',
annotations: {
readOnlyHint: true,
idempotentHint: true,
},
inputSchema: {
code: z
.string()
.optional()
.describe('Gerbil source code to compile-check'),
file_path: z
.string()
.optional()
.describe(
'Path to a .ss/.scm file to compile-check (alternative to code)',
),
loadpath: z
.array(z.string())
.optional()
.describe(
'Directories to add to GERBIL_LOADPATH for project-local module resolution',
),
},
},
async ({ code, file_path, loadpath }) => {
if (!code && !file_path) {
return {
content: [
{
type: 'text' as const,
text: 'Either "code" or "file_path" must be provided.',
},
],
isError: true,
};
}
let targetPath = file_path || '';
let tempFile = false;
// Write code to temp file if provided inline
if (code) {
const tempName = `gerbil-check-${randomUUID().slice(0, 8)}.ss`;
targetPath = join(tmpdir(), tempName);
try {
await writeFile(targetPath, code, 'utf-8');
tempFile = true;
} catch (err) {
const msg =
err instanceof Error
? err.message
: 'Unknown error writing temp file';
return {
content: [
{
type: 'text' as const,
text: `Failed to write temp file: ${msg}`,
},
],
isError: true,
};
}
}
try {
const env = loadpath && loadpath.length > 0 ? buildLoadpathEnv(loadpath) : undefined;
const result = await runGxc(targetPath, { env });
if (result.timedOut) {
return {
content: [
{
type: 'text' as const,
text: 'Compilation check timed out after 30 seconds.',
},
],
isError: true,
};
}
if (result.exitCode === 127) {
return {
content: [
{
type: 'text' as const,
text: 'gxc compiler not found. Ensure Gerbil is installed and gxc is in PATH.',
},
],
isError: true,
};
}
if (result.exitCode !== 0) {
// Combine stdout and stderr for error output — gxc may write
// errors to either stream depending on the error type
let errorOutput = result.stderr.trim();
const stdoutOutput = result.stdout.trim();
if (!errorOutput && stdoutOutput) {
errorOutput = stdoutOutput;
} else if (errorOutput && stdoutOutput) {
errorOutput = errorOutput + '\n' + stdoutOutput;
}
// Replace temp file path with "<input>" for cleaner output
if (tempFile && errorOutput) {
errorOutput = errorOutput.replaceAll(targetPath, '<input>');
}
if (!errorOutput) {
return {
content: [
{
type: 'text' as const,
text: `Compilation failed with exit code ${result.exitCode} (no error details available). Try gerbil_diagnostics for more info.`,
},
],
isError: true,
};
}
const enhanced = enhanceGxcError(errorOutput);
return {
content: [
{
type: 'text' as const,
text: `Compilation errors found:\n\n${enhanced}`,
},
],
isError: true,
};
}
// Success
const target = tempFile ? 'Code' : targetPath;
return {
content: [
{
type: 'text' as const,
text: `Compilation check passed. ${target} compiled successfully (gxc -S).`,
},
],
};
} finally {
// Clean up temp file
if (tempFile) {
try {
await unlink(targetPath);
} catch {
// ignore cleanup errors
}
}
}
},
);
}
function enhanceGxcError(errorOutput: string): string {
const sections: string[] = [errorOutput];
// Detect known compiler-internal crash patterns
const internalPatterns = [
{
pattern: /stx-car-e/,
hint: 'The compiler tried to destructure a non-pair syntax object (stx-car-e). This usually means a macro or form produced unexpected syntax. Try simplifying the expression — e.g., replace (or ...) with (let ((v ...)) (if v v ...)).',
},
{
pattern: /gx-core-expand/,
hint: 'The compiler crashed during core expansion. The code may use a macro form incorrectly.',
},
{
pattern: /code generation/i,
hint: 'The compiler crashed during code generation. This may be a gxc bug or an unsupported code pattern.',
},
{
pattern: /Segmentation fault|SIGSEGV/,
hint: 'The compiler segfaulted. This is almost certainly a gxc bug.',
},
{
pattern: /##raise-heap-overflow/,
hint: 'The compiler ran out of heap space. The code may have deeply nested or recursive macro expansions.',
},
];
for (const { pattern, hint } of internalPatterns) {
if (pattern.test(errorOutput)) {
sections.push('');
sections.push(`Hint: ${hint}`);
break;
}
}
// Detect "cannot find library module" and check for stale .ssi artifacts
const moduleNotFound = errorOutput.match(
/cannot find library module\s+:?([^\s;)]+)/,
);
if (moduleNotFound) {
const modPath = moduleNotFound[1].replace(/^:/, '');
const staleHints = detectStaleModule(modPath);
if (staleHints.length > 0) {
sections.push('');
sections.push('Stale artifact diagnostic:');
for (const h of staleHints) {
sections.push(` ${h}`);
}
} else {
sections.push('');
sections.push(
`Hint: "cannot find library module :${modPath}" — this may indicate: ` +
'(1) the module has not been compiled yet (run gerbil build), ' +
'(2) GERBIL_LOADPATH is missing the project\'s .gerbil/lib directory, ' +
'(3) stale .ssi artifacts — try "make clean" or delete .gerbil/lib/ and rebuild.',
);
}
}
// Try to extract source location from expansion context
const contextMatch = errorOutput.match(
/--- expansion context ---[\s\S]*?at:\s+(.+)/,
);
if (contextMatch) {
sections.push('');
sections.push(`Expansion context location: ${contextMatch[1]}`);
}
// Extract the first "at:" location if present (but not if already found above)
if (!contextMatch) {
const atMatch = errorOutput.match(/at:\s+([^\n]+)/);
if (atMatch) {
sections.push('');
sections.push(`Error location: ${atMatch[1]}`);
}
}
return sections.join('\n');
}
/**
* Check if a module path has stale .ssi artifacts.
* Looks in .gerbil/lib/ and ~/.gerbil/lib/ for compiled artifacts
* that are older than corresponding source files.
*/
function detectStaleModule(modPath: string): string[] {
const hints: string[] = [];
const parts = modPath.split('/');
const relPath = parts.join('/');
const gerbilPath = process.env.GERBIL_PATH ?? join(homedir(), '.gerbil');
const searchDirs = [
join(gerbilPath, 'lib'),
// Also check CWD-local .gerbil/lib
join(process.cwd(), '.gerbil', 'lib'),
];
for (const libDir of searchDirs) {
for (const ext of ['.ssi', '.scm', '.o', '.o1']) {
const artPath = join(libDir, relPath + ext);
try {
if (existsSync(artPath)) {
const artStat = statSync(artPath);
// Look for source .ss file in common locations
const possibleSources = [
join(process.cwd(), relPath + '.ss'),
join(process.cwd(), parts[parts.length - 1] + '.ss'),
];
for (const srcPath of possibleSources) {
try {
if (existsSync(srcPath)) {
const srcStat = statSync(srcPath);
if (srcStat.mtimeMs > artStat.mtimeMs) {
hints.push(
`Found stale ${ext} artifact: ${artPath} ` +
`(artifact: ${artStat.mtime.toISOString().slice(0, 19)}, ` +
`source: ${srcStat.mtime.toISOString().slice(0, 19)}). ` +
`Delete it and rebuild.`,
);
}
}
} catch { /* skip */ }
}
// Even if we can't find source, report the artifact exists
if (hints.length === 0) {
hints.push(
`Found compiled artifact: ${artPath}. ` +
`If this module was recently modified, this artifact may be stale — ` +
`delete it and rebuild.`,
);
}
}
} catch { /* skip */ }
}
}
return hints;
}