compiler.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. /** Shared contract for the Pascal compiler service (tutorial section 27).
  2. *
  3. * The frontend never spawns the compiler directly: it calls this service
  4. * over RPC and the backend runs `fpc` asynchronously.
  5. * Other compilers can implement the same interface without changing the rest of the IDE.
  6. */
  7. export const PascalCompilerServicePath = '/services/pascal-compiler';
  8. export const PascalCompiler = Symbol('PascalCompiler');
  9. export type BuildSeverity = 'error' | 'warning' | 'info';
  10. export interface BuildDiagnostic {
  11. /** File path as reported by the compiler (relative to the project root). */
  12. file: string;
  13. /** Zero-based line and character. */
  14. line: number;
  15. character: number;
  16. message: string;
  17. severity: BuildSeverity;
  18. }
  19. export interface BuildResult {
  20. success: boolean;
  21. exitCode: number;
  22. /** Exact command line that was executed (for transparency). */
  23. command: string;
  24. /** Combined stdout/stderr of the compiler. */
  25. output: string;
  26. diagnostics: BuildDiagnostic[];
  27. /** Project source statistics and output size (present on success). */
  28. stats?: BuildStats;
  29. }
  30. /** Source census for the build report dialog. */
  31. export interface BuildStats {
  32. /** Number of `.pas`/`.pp` files under the project root. */
  33. files: number;
  34. /** Total lines across those files. */
  35. lines: number;
  36. /** Built executable size in bytes (-1 when unavailable). */
  37. outputSize: number;
  38. }
  39. /** Human-readable byte count (`45.2 KB`, `3.1 MB`). Pure and testable. */
  40. export function formatBytes(bytes: number): string {
  41. if (!Number.isFinite(bytes) || bytes < 0) return '—';
  42. if (bytes < 1024) return `${bytes} B`;
  43. const kb = bytes / 1024;
  44. if (kb < 1024) return `${kb.toFixed(1)} KB`;
  45. return `${(kb / 1024).toFixed(1)} MB`;
  46. }
  47. export type CompilerType = 'fpc' | 'blaise';
  48. export interface CompilerOptions {
  49. /** Compiler backend: "fpc" (default) or the Blaise compiler. */
  50. type?: CompilerType;
  51. /** Compiler executable, e.g. "fpc" or an absolute path. */
  52. path: string;
  53. options: string[];
  54. }
  55. export interface PascalProject {
  56. name: string;
  57. /** Absolute workspace folder containing pascal.json. */
  58. root: string;
  59. /** Main file, relative to root (e.g. "src/Main.pas"). */
  60. main: string;
  61. /** Built executable, relative to root. Defaults to bin/<name>. */
  62. output?: string;
  63. compiler: CompilerOptions;
  64. /** True when pascal.json sets an explicit compiler path. */
  65. compilerPathExplicit: boolean;
  66. }
  67. export interface CompilerInfo {
  68. path: string;
  69. /** First line of `fpc --version`. */
  70. version: string;
  71. }
  72. export interface PascalCompiler {
  73. /** Probe a compiler executable; undefined when it cannot be run. */
  74. detect(compilerPath?: string): Promise<CompilerInfo | undefined>;
  75. /** Compile (and link) the project main file. */
  76. build(project: PascalProject): Promise<BuildResult>;
  77. /** Remove build outputs (best-effort). */
  78. clean(project: PascalProject): Promise<void>;
  79. /** Build, then execute the produced program and capture its output. */
  80. run(project: PascalProject): Promise<BuildResult>;
  81. }
  82. /** Minimal `pascal.json` reader (tutorial sections 23/33). Pure and testable. */
  83. export function parseProjectFile(root: string, jsonText: string): PascalProject {
  84. const raw = JSON.parse(jsonText) as {
  85. name?: unknown; main?: unknown; output?: unknown;
  86. compiler?: { type?: unknown; path?: unknown; options?: unknown };
  87. };
  88. if (typeof raw.name !== 'string' || raw.name.length === 0) {
  89. throw new Error('pascal.json: missing "name".');
  90. }
  91. if (typeof raw.main !== 'string' || raw.main.length === 0) {
  92. throw new Error('pascal.json: missing "main".');
  93. }
  94. const compiler = raw.compiler ?? {};
  95. const type = compiler.type === 'blaise' ? 'blaise' as const : 'fpc' as const;
  96. const options = Array.isArray(compiler.options)
  97. ? compiler.options.filter((o): o is string => typeof o === 'string')
  98. : [];
  99. const explicitPath = typeof compiler.path === 'string' && compiler.path.length > 0;
  100. return {
  101. name: raw.name,
  102. root,
  103. main: raw.main,
  104. output: typeof raw.output === 'string' ? raw.output : undefined,
  105. compiler: {
  106. type,
  107. path: explicitPath ? (compiler.path as string) : (type === 'blaise' ? 'blaise' : 'fpc'),
  108. options,
  109. },
  110. compilerPathExplicit: explicitPath,
  111. };
  112. }
  113. /** Default executable path for a project (bin/<name> under root). */
  114. export function defaultOutput(project: PascalProject): string {
  115. return project.output ?? `bin/${project.name}`;
  116. }
  117. export interface ProvidedTask {
  118. label: string;
  119. group?: { kind: 'build' | 'rebuild'; isDefault?: boolean } | 'clean';
  120. command: string;
  121. args: string[];
  122. dependsOn?: string;
  123. }
  124. /** Pure task list for a project (tested headlessly; the Theia provider wraps these). */
  125. export function buildProvidedTasks(project: PascalProject): ProvidedTask[] {
  126. const out = defaultOutput(project);
  127. const buildArgs = project.compiler.type === 'blaise'
  128. ? ['--source', project.main, '--output', out, ...project.compiler.options]
  129. : ['-g', ...project.compiler.options, '-o' + out, project.main];
  130. return [
  131. { label: 'Pascal: Build', group: { kind: 'build', isDefault: true }, command: project.compiler.path, args: buildArgs },
  132. { label: 'Pascal: Clean', group: 'clean', command: 'rm', args: ['-f', out] },
  133. { label: 'Pascal: Rebuild', group: { kind: 'rebuild' }, command: project.compiler.path, args: buildArgs },
  134. { label: 'Pascal: Run', command: `./${out}`, args: [], dependsOn: 'Pascal: Build' },
  135. ];
  136. }