# 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, builtin-procedure signatures, record-field types, `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. ## 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.