/** Shared contract for the Pascal compiler service (tutorial section 27). * * The frontend never spawns the compiler directly: it calls this service * over RPC and the backend runs `fpc` asynchronously. * Other compilers can implement the same interface without changing the rest of the IDE. */ export const PascalCompilerServicePath = '/services/pascal-compiler'; export const PascalCompiler = Symbol('PascalCompiler'); export type BuildSeverity = 'error' | 'warning' | 'info'; export interface BuildDiagnostic { /** File path as reported by the compiler (relative to the project root). */ file: string; /** Zero-based line and character. */ line: number; character: number; message: string; severity: BuildSeverity; } export interface BuildResult { success: boolean; exitCode: number; /** Exact command line that was executed (for transparency). */ command: string; /** Combined stdout/stderr of the compiler. */ output: string; diagnostics: BuildDiagnostic[]; /** Project source statistics and output size (present on success). */ stats?: BuildStats; } /** Source census for the build report dialog. */ export interface BuildStats { /** Number of `.pas`/`.pp` files under the project root. */ files: number; /** Total lines across those files. */ lines: number; /** Built executable size in bytes (-1 when unavailable). */ outputSize: number; } /** Human-readable byte count (`45.2 KB`, `3.1 MB`). Pure and testable. */ export function formatBytes(bytes: number): string { if (!Number.isFinite(bytes) || bytes < 0) return '—'; if (bytes < 1024) return `${bytes} B`; const kb = bytes / 1024; if (kb < 1024) return `${kb.toFixed(1)} KB`; return `${(kb / 1024).toFixed(1)} MB`; } export type CompilerType = 'fpc' | 'blaise'; export interface CompilerOptions { /** Compiler backend: "fpc" (default) or the Blaise compiler. */ type?: CompilerType; /** Compiler executable, e.g. "fpc" or an absolute path. */ path: string; options: string[]; } export interface PascalProject { name: string; /** Absolute workspace folder containing pascal.json. */ root: string; /** Main file, relative to root (e.g. "src/Main.pas"). */ main: string; /** Built executable, relative to root. Defaults to bin/. */ output?: string; compiler: CompilerOptions; /** True when pascal.json sets an explicit compiler path. */ compilerPathExplicit: boolean; } export interface CompilerInfo { path: string; /** First line of `fpc --version`. */ version: string; } export interface PascalCompiler { /** Probe a compiler executable; undefined when it cannot be run. */ detect(compilerPath?: string): Promise; /** Compile (and link) the project main file. */ build(project: PascalProject): Promise; /** Remove build outputs (best-effort). */ clean(project: PascalProject): Promise; /** Build, then execute the produced program and capture its output. */ run(project: PascalProject): Promise; } /** Minimal `pascal.json` reader (tutorial sections 23/33). Pure and testable. */ export function parseProjectFile(root: string, jsonText: string): PascalProject { const raw = JSON.parse(jsonText) as { name?: unknown; main?: unknown; output?: unknown; compiler?: { type?: unknown; path?: unknown; options?: unknown }; }; if (typeof raw.name !== 'string' || raw.name.length === 0) { throw new Error('pascal.json: missing "name".'); } if (typeof raw.main !== 'string' || raw.main.length === 0) { throw new Error('pascal.json: missing "main".'); } const compiler = raw.compiler ?? {}; const type = compiler.type === 'blaise' ? 'blaise' as const : 'fpc' as const; const options = Array.isArray(compiler.options) ? compiler.options.filter((o): o is string => typeof o === 'string') : []; const explicitPath = typeof compiler.path === 'string' && compiler.path.length > 0; return { name: raw.name, root, main: raw.main, output: typeof raw.output === 'string' ? raw.output : undefined, compiler: { type, path: explicitPath ? (compiler.path as string) : (type === 'blaise' ? 'blaise' : 'fpc'), options, }, compilerPathExplicit: explicitPath, }; } /** Default executable path for a project (bin/ under root). */ export function defaultOutput(project: PascalProject): string { return project.output ?? `bin/${project.name}`; } export interface ProvidedTask { label: string; group?: { kind: 'build' | 'rebuild'; isDefault?: boolean } | 'clean'; command: string; args: string[]; dependsOn?: string; } /** Pure task list for a project (tested headlessly; the Theia provider wraps these). */ export function buildProvidedTasks(project: PascalProject): ProvidedTask[] { const out = defaultOutput(project); const buildArgs = project.compiler.type === 'blaise' ? ['--source', project.main, '--output', out, ...project.compiler.options] : ['-g', ...project.compiler.options, '-o' + out, project.main]; return [ { label: 'Pascal: Build', group: { kind: 'build', isDefault: true }, command: project.compiler.path, args: buildArgs }, { label: 'Pascal: Clean', group: 'clean', command: 'rm', args: ['-f', out] }, { label: 'Pascal: Rebuild', group: { kind: 'rebuild' }, command: project.compiler.path, args: buildArgs }, { label: 'Pascal: Run', command: `./${out}`, args: [], dependsOn: 'Pascal: Build' }, ]; }