DIAGNOSTICS.md 6.8 KB

Modula-2 diagnostics — step 1

This first diagnostics layer is deliberately lightweight. It does not replace Coco/R. It provides immediate editor feedback for a small set of structural errors while the real parser/LSP is being prepared.

It currently checks:

  • unterminated (* ... *) comments;
  • unterminated string literals;
  • presence of the module header;
  • ; after the module header;
  • presence of the final END <ModuleName>.;
  • module-name consistency between the header and final END;
  • the final period after END <ModuleName>.

The diagnostics are published through the VS Code-compatible DiagnosticCollection, which Theia displays directly in the editor.

The next stage will replace the structural checks with diagnostics generated by the Coco/R parser, while keeping the same editor diagnostic mechanism.

Server-side semantic layer (src/m2/analyse.ts, source modula2)

In parallel, the language server runs its own checks on every open document — always on, merged with Coco/R output when a validator is configured:

  • duplicate declarations in one scope (error, with first-declared line);
  • unknown identifiers (error), with conservative skips: qualified names, import clauses, WITH regions, unverifiable FROM-imports, predefined identifiers, closing END Name;
  • enumeration literals (A, B, ...) are treated as declared constants, so they resolve, complete, and appear in the outline.

Not covered (needs statement/expression parsing): assignment type compatibility, call arity, use-before-declaration order, FROM-list membership against the defining .def.

Statement checker (src/m2/checker.ts + src/m2/types.ts, source modula2)

Bodies (BEGIN..END, recorded by the parser) are walked with a recursive statement/expression analyser. Operator and compatibility rules were calibrated case-by-case against gm2:

  • assignment compatibility, including whole-number family, REAL↔LONGREAL, CHAR/string rules with ARRAY OF CHAR length checks, sets, pointers, enumerations, NIL;
  • call arity (GNU optional parameters supported) and argument types, including open-array formals; ignored function results and void procedures used as values;
  • BOOLEAN conditions (IF, WHILE, ELSIF, UNTIL), ordinal CASE selectors and FOR variables/bounds/BY steps;
  • illegal targets (CONST, procedure, type, module), illegal operands (NOT/AND/OR, mixed arithmetic, DIV/MOD on reals, indexing, dereferencing, comparisons of arrays/records, CARDINAL vs INTEGER comparisons);
  • RETURN without a value, value-returning RETURNs, RETURN outside procedures and values returned from void procedures.

Still out of scope: use-before-declaration order, range checking, WITH-block internals.

Real-file validation

The checker is validated against a real codebase — the six modules of the m2compiler-V3 compiler (FileIO, SymTab, QbeGen, M2S, M2P, M2), all of which gm2 -fiso accepts with zero errors. The analyser reports zero issues on all six. Getting there exposed and fixed:

  • implementation modules see their own .def (FileIO.mod + FileIO.def);
  • FORWARD procedure headings (not duplicates of the later definition);
  • field access after ^/[] (f^.x, a[i].x) and flattened multi-index (a[i, j] over nested arrays);
  • octal/hex/char literal suffixes (0C is CHAR, 177B/0FFH whole);
  • no backslash escapes in strings ('\' is one character);
  • type arguments to builtins (SIZE(T), VAL(T, x), MAX(T), SYSTEM.TSIZE(T)) and typed set constructors (BITSET{...});
  • opaque types (TYPE File;), local-module EXPORT visibility, procedure variables (calls and P := Q assignment), the pervasive LENGTH/ORDL, arithmetic that preserves CARDINAL (c + 1).

Record-field types

Record declarations capture each field's type, so r.f, r.a.b, p^.f, a[i].f and F().f resolve to real types instead of unknown: field reads, writes and comparisons are checked, and hover/go-to-definition land on the field declaration. Genuine errors are reported too: selecting a field of a known non-record (f.x on a POINTER — Modula-2 does not implicitly dereference) and unknown fields (r.nope). Anything unresolvable stays silent.

Nominal comparison is by declaration identity, not spelling: R and M.R naming the same .def declaration are the same type, while two same-shaped records from different declarations are not.

Builtin-procedure signatures

Pervasive builtins are arity- and argument-checked against rules probed case by case against real gm2 (16.0.1), which is loose in surprising places (ORD/CHR/FLOAT accept BITSET; CAP accepts INTEGER but not enumerations; INCL's message claims "1 or 2 parameters" but every one-argument form is rejected) and even crashes on some invalid calls (ODD(1, 2), ODD(r), ABS of a record) — the checker reports proper errors there instead. Uncertain cases stay silent: HALT/NEW/DISPOSE take any argument list, and the murky LENGTH/TSIZE/CODE/COPY/ FILL (undefined unqualified in this gm2) are unchecked, as before.

Checked: INC/DEC (1–2 args, variable ordinal target, ordinal step), INCL/EXCL (exactly 2 args, set variable plus CARDINAL-family element — an INTEGER variable is rejected), ABS (numeric/set source, result keeps the argument type), CAP (CHAR/whole source, CHAR result), CHR/ORD/ORDL (scalar/set source; CHAR/CARDINAL/ LONGCARD results), ODD (whole-number-or-enum source, BOOLEAN result), FLOAT/LFLOAT (scalar/set source, REAL/LONGREAL results), TRUNC (real source, INTEGER result), VAL (type plus scalar source, result is the named type), SIZE (any single argument, CARDINAL constant), MAX/MIN (one type or value, never a record; sets and pointers yield CARDINAL, arrays their element type), ADR (variable, procedure or string constant; ADDRESS result). Function builtins used as statements and void builtins used as values are reported through the existing ignored-result/void paths. A 178-case differential suite (gm2 accept ⇒ zero diagnostics; gm2 reject ⇒ at least one) passes with no mismatches, and the real-file corpora stay at zero issues.

Circular dependencies (no use-before-declaration)

gm2 is multi-pass, so declaration order never matters — there is deliberately no use-before-declaration check. What is checked instead is dependency cycles that never ground, which gm2 rejects: A = B; B = A, A = POINTER TO B; B = POINTER TO A, T = POINTER TO T, constant cycles (a = b; b = a), and aliases through procedure types. Chains grounding at a RECORD, enumeration, predefined type or unknown stay silent, as does the callback pattern (H = PROCEDURE (e: E) with E a record). Only same-file declarations are followed, so reported positions always belong to the open document.