# 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 .`; - module-name consistency between the header and final `END`; - the final period after `END `. 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 `RETURN`s, `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.