/** GNU Modula-2 implementation of the compiler service. * * Runs `gm2` as a child process (never blocking the frontend) and turns * its GCC-style `file:line:col: severity: message` output into structured * diagnostics. `parseGm2Diagnostics` is exported pure for testing. */ import { injectable } from '@theia/core/shared/inversify'; import { spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { BuildDiagnostic, BuildResult, BuildStats, CompilerInfo, Modula2Compiler, Modula2Project, defaultOutput, } from '../common/compiler'; export interface ProcessResult { exitCode: number; output: string; } /** Source census plus output size for the build report (best-effort). */ export async function collectBuildStats( project: Modula2Project, out: string, ): Promise { let files = 0; let lines = 0; const visit = async (dir: string, depth: number): Promise => { if (depth > 8) return; let entries: import('fs').Dirent[]; try { entries = await fs.promises.readdir(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; await visit(full, depth + 1); } else if (entry.name.endsWith('.mod') || entry.name.endsWith('.def')) { files++; try { const text = await fs.promises.readFile(full, 'utf8'); lines += text.split('\n').length; } catch { // Count the file even when its lines cannot be read. } } } }; await visit(project.root, 0); let outputSize = -1; try { outputSize = (await fs.promises.stat(path.join(project.root, out))).size; } catch { // Leave unavailable. } return { files, lines, outputSize }; } export function runProcess(command: string, args: string[], cwd: string): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd, shell: false }); let output = ''; child.stdout.on('data', d => { output += d.toString(); }); child.stderr.on('data', d => { output += d.toString(); }); child.on('error', reject); child.on('close', code => resolve({ exitCode: code ?? 1, output })); }); } const GM2_LINE = /^(.+?):(\d+):(\d+):\s+(warning|error|note|fatal error):\s+(.*)$/; /** Parse GCC-style diagnostics; context/caret lines are ignored. */ export function parseGm2Diagnostics(output: string): BuildDiagnostic[] { const diagnostics: BuildDiagnostic[] = []; for (const line of output.split('\n')) { const m = GM2_LINE.exec(line); if (!m) continue; const severity = m[4].includes('error') ? 'error' : m[4] === 'warning' ? 'warning' : 'info'; diagnostics.push({ file: m[1], line: Math.max(0, Number(m[2]) - 1), character: Math.max(0, Number(m[3]) - 1), message: m[5].trim(), severity, }); } return diagnostics; } @injectable() export class GnuModula2CompilerService implements Modula2Compiler { async detect(compilerPath = 'gm2'): Promise { try { const result = await runProcess(compilerPath, ['--version'], process.cwd()); if (result.exitCode !== 0) return undefined; const version = (result.output.split('\n')[0] ?? '').trim(); return { path: compilerPath, version }; } catch { return undefined; } } async build(project: Modula2Project): Promise { const out = defaultOutput(project); await fs.promises.mkdir(path.dirname(path.join(project.root, out)), { recursive: true }); const args = ['-fiso', '-g', ...project.compiler.options, '-o', out, project.main]; const command = [project.compiler.path, ...args].join(' '); let result: ProcessResult; try { result = await runProcess(project.compiler.path, args, project.root); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { success: false, exitCode: 127, command, output: `Could not start compiler: ${message}`, diagnostics: [], }; } const success = result.exitCode === 0; return { success, exitCode: result.exitCode, command, output: result.output, diagnostics: parseGm2Diagnostics(result.output), stats: success ? await collectBuildStats(project, out) : undefined, }; } async clean(project: Modula2Project): Promise { const out = path.join(project.root, defaultOutput(project)); await fs.promises.rm(out, { force: true }); // Best-effort removal of compiler side-products next to the sources. const srcDir = path.dirname(path.join(project.root, project.main)); let entries: string[] = []; try { entries = await fs.promises.readdir(srcDir); } catch { return; } await Promise.all(entries .filter(e => e.endsWith('.o') || e.endsWith('.s') || e.endsWith('.lst')) .map(e => fs.promises.rm(path.join(srcDir, e), { force: true }))); } async run(project: Modula2Project): Promise { const built = await this.build(project); if (!built.success) return built; const executable = path.join(project.root, defaultOutput(project)); let result: ProcessResult; try { result = await runProcess(executable, [], project.root); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { success: false, exitCode: 127, command: executable, output: `Could not start program: ${message}`, diagnostics: [], }; } return { success: result.exitCode === 0, exitCode: result.exitCode, command: executable, output: result.output, diagnostics: result.exitCode === 0 ? [] : [{ file: project.main, line: 0, character: 0, message: `Program exited with code ${result.exitCode}.`, severity: 'error', }], }; } }