Browse Source

Language server: Pascal dialect validators via LSP

Eric Streit 2 ngày trước cách đây
mục cha
commit
7ada4e24d9

+ 28 - 0
extensions/modula2-language/COCOR-LSP.md

@@ -83,3 +83,31 @@ consumers must parse stdout.
   Two probes were correctly *not* errors: gm2 itself only warns on a
   missing `;` before `END` and on an unclosed `(` (separator/recovery
   leniency), and GM2 agrees with it.
+
+## Pascal dialects (`.pas` / `.pp` / `.p`)
+
+The same server validates Pascal files with the pilot validators from
+`grammars/`. Each pilot binary takes the source file as its single
+argument and writes a `.LST` listing next to it; the server parses the
+listing's `***** ^ message` lines (positions are caret-relative to the
+code after the listing prefix, zero-based columns).
+
+```json
+"modula2.pascal.dialect": "freepascal",
+"modula2.pascal.validators": {
+  "p2": "<root>/grammars/P2/build/P2",
+  "p4": "<root>/grammars/P4/build/P4",
+  "p6": "<root>/grammars/P6/build/P6",
+  "pascal": "<root>/grammars/Pascal/build/PStd",
+  "turbopascal3": "<root>/grammars/TurboPascal3/build/TP3",
+  "freepascal": "<root>/grammars/FreePascal/build/FPC",
+  "blaise": "<root>/grammars/Blaise/build/BlaiseV",
+  "pascals": "<root>/grammars/PascalS/build/PascalS"
+}
+```
+
+Build each binary first with its pilot `build.sh` (needs `CR` and
+`gm2 -fiso`). The local Modula-2 analysis never runs on Pascal text.
+With no command configured for the active dialect the server publishes
+no diagnostics. Never point these at the `pcom`/`psc` oracle compilers
+— they can hang on invalid input.

+ 35 - 1
extensions/modula2-language/package.json

@@ -12,7 +12,8 @@
   ],
   "activationEvents": [
     "onLanguage:modula2",
-    "onLanguage:modula2-definition"
+    "onLanguage:modula2-definition",
+    "onLanguage:pascal"
   ],
   "main": "./out/extension.js",
   "contributes": {
@@ -38,6 +39,19 @@
           ".def"
         ],
         "configuration": "./language-configuration.json"
+      },
+      {
+        "id": "pascal",
+        "aliases": [
+          "Pascal",
+          "pascal"
+        ],
+        "extensions": [
+          ".pas",
+          ".pp",
+          ".p"
+        ],
+        "configuration": "./language-configuration.json"
       }
     ],
     "grammars": [
@@ -148,6 +162,26 @@
             "{file}"
           ],
           "description": "Arguments passed to the Coco/R validator. Use {file} for the temporary source file."
+        },
+        "modula2.pascal.dialect": {
+          "type": "string",
+          "enum": [
+            "p2",
+            "p4",
+            "p6",
+            "pascal",
+            "turbopascal3",
+            "freepascal",
+            "blaise",
+            "pascals"
+          ],
+          "default": "freepascal",
+          "description": "Pascal dialect used to validate .pas files. Builds: grammars/P2/build/P2, P4/build/P4, P6/build/P6, Pascal/build/PStd, TurboPascal3/build/TP3, FreePascal/build/FPC, Blaise/build/BlaiseV, PascalS/build/PascalS."
+        },
+        "modula2.pascal.validators": {
+          "type": "object",
+          "default": {},
+          "description": "Validator command per Pascal dialect, e.g. {\"freepascal\": \"/path/to/FPC\"}. Each command receives the temporary source file as its single argument. Never point these at pcom/psc oracle compilers (they can hang)."
         }
       }
     }

+ 4 - 1
extensions/modula2-language/src/extension.ts

@@ -26,12 +26,15 @@ export function activate(context: vscode.ExtensionContext): void {
   const clientOptions: LanguageClientOptions = {
     documentSelector: [
       { scheme: 'file', language: 'modula2' },
-      { scheme: 'file', language: 'modula2-definition' }
+      { scheme: 'file', language: 'modula2-definition' },
+      { scheme: 'file', language: 'pascal' }
     ],
     synchronize: { configurationSection: 'modula2' },
     initializationOptions: {
       validatorCommand: cfg.get<string>('cocoR.validatorCommand', ''),
       validatorArguments: cfg.get<string[]>('cocoR.validatorArguments', ['{file}']),
+      pascalDialect: cfg.get<string>('pascal.dialect', 'freepascal'),
+      pascalValidators: cfg.get<object>('pascal.validators', {}),
       format: {
         indentSize: cfg.get<number>('format.indentSize', 2),
         useTabs: cfg.get<boolean>('format.useTabs', false),

+ 28 - 0
extensions/modula2-language/src/pascalListing.ts

@@ -0,0 +1,28 @@
+import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver/node';
+
+/** Parse a Coco/R `.LST` listing into LSP diagnostics.
+ *
+ *  Error lines look like `*****       ^ invalid Primary` and refer to
+ *  the source line above them (`    4  I := ;`); the caret column is
+ *  relative to the code after the listing's line-number prefix.
+ */
+export function parseListing(listing: string): Diagnostic[] {
+  const result: Diagnostic[] = [];
+  const lines = listing.split('\n');
+  for (let i = 1; i < lines.length; i++) {
+    const m = /^\*+\s*\^\s*(.*)$/.exec(lines[i]);
+    if (!m) continue;
+    const prev = lines[i - 1];
+    const pm = /^\s*(\d+)\s/.exec(prev);
+    if (!pm) continue;
+    const prefix = /^\s*\d+\s*/.exec(prev);
+    const prefixLen = prefix ? prefix[0].length : 0;
+    const line = Math.max(0, Number(pm[1]) - 1);
+    const character = Math.max(0, lines[i].indexOf('^') - prefixLen);
+    const message = (m[1] || '').trim() || 'syntax error';
+    result.push({ severity: DiagnosticSeverity.Error, range: {
+      start: { line, character }, end: { line, character: character + 1 }
+    }, message, source: 'Pascal' });
+  }
+  return result;
+}

+ 54 - 0
extensions/modula2-language/src/server.ts

@@ -11,6 +11,7 @@ import { promises as fs, readFileSync } from 'fs';
 import { spawn } from 'child_process';
 import { pathToFileURL } from 'url';
 import { lex, Token } from './m2/lexer';
+import { parseListing } from './pascalListing';
 import { M2Symbol, M2Unit, parseUnitText } from './m2/parser';
 import { analyseUnit } from './m2/analyse';
 import { computeSemanticTokens, semanticTokensLegend } from './m2/semantic';
@@ -24,15 +25,20 @@ const connection = createConnection(ProposedFeatures.all);
 const documents = new TextDocuments(TextDocument);
 let validatorCommand = '';
 let validatorArguments: string[] = ['{file}'];
+let pascalDialect = 'freepascal';
+let pascalValidators: Record<string, string> = {};
 let formatOptions: FormatOptions = { ...DEFAULT_FORMAT_OPTIONS };
 
 connection.onInitialize((params: InitializeParams): InitializeResult => {
   const options = (params.initializationOptions || {}) as {
     validatorCommand?: string; validatorArguments?: string[];
+    pascalDialect?: string; pascalValidators?: Record<string, string>;
     format?: Partial<FormatOptions>;
   };
   validatorCommand = options.validatorCommand || '';
   validatorArguments = options.validatorArguments || ['{file}'];
+  pascalDialect = options.pascalDialect || 'freepascal';
+  pascalValidators = options.pascalValidators || {};
   formatOptions = normalizeFormatOptions(options.format ?? {});
   return {
     capabilities: {
@@ -57,11 +63,14 @@ connection.onDidChangeConfiguration((params: DidChangeConfigurationParams) => {
   const settings = (params.settings || {}) as {
     modula2?: {
       cocoR?: { validatorCommand?: string; validatorArguments?: string[] };
+      pascal?: { dialect?: string; validators?: Record<string, string> };
       format?: Partial<FormatOptions>;
     };
   };
   validatorCommand = settings.modula2?.cocoR?.validatorCommand || validatorCommand;
   validatorArguments = settings.modula2?.cocoR?.validatorArguments || validatorArguments;
+  pascalDialect = settings.modula2?.pascal?.dialect || pascalDialect;
+  pascalValidators = settings.modula2?.pascal?.validators || pascalValidators;
   if (settings.modula2?.format) {
     formatOptions = normalizeFormatOptions(settings.modula2.format);
   }
@@ -72,6 +81,10 @@ documents.onDidChangeContent(e => validate(e.document));
 documents.onDidClose(e => connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] }));
 
 async function validate(document: TextDocument): Promise<void> {
+  if (document.languageId === 'pascal') {
+    await validatePascal(document);
+    return;
+  }
   const local = analyseDocument(document);
   if (!validatorCommand) {
     connection.sendDiagnostics({ uri: document.uri, diagnostics: local });
@@ -100,6 +113,47 @@ async function validate(document: TextDocument): Promise<void> {
   }
 }
 
+/** Validate a Pascal document with the configured dialect validator.
+ *
+ *  The local Modula-2 analysis never runs on Pascal text. Each pilot
+ *  validator takes the source file as its single argument and writes
+ *  `<base>.LST` next to it; diagnostics come from that listing, whose
+ *  `***** ^ message` lines point at the preceding source line.
+ *  No validator configured (or dialect unknown) means no diagnostics.
+ */
+async function validatePascal(document: TextDocument): Promise<void> {
+  const command = pascalValidators[pascalDialect];
+  if (!command) {
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
+    return;
+  }
+  const filePath = uriToFilePath(document.uri);
+  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pascal-lsp-'));
+  const base = path.basename(filePath, path.extname(filePath));
+  const tempFile = path.join(dir, base + '.pas');
+  try {
+    await fs.writeFile(tempFile, document.getText(), 'utf8');
+    await execute(command, [tempFile], path.dirname(filePath));
+    let listing = '';
+    try {
+      listing = await fs.readFile(path.join(dir, base + '.LST'), 'utf8');
+    } catch {
+      listing = '';
+    }
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: parseListing(listing) });
+  } catch (error) {
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: [{
+      severity: DiagnosticSeverity.Warning,
+      range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
+      message: `Pascal validator: ${error instanceof Error ? error.message : String(error)}`,
+      source: 'Pascal'
+    }] });
+  } finally {
+    await fs.rm(dir, { recursive: true, force: true });
+  }
+}
+
+
 function execute(command: string, args: string[], cwd: string): Promise<string> {
   return new Promise((resolve, reject) => {
     const child = spawn(command, args, { cwd, shell: false });