# 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. ## 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`). ## Definition search paths Go-to-definition (and hover, completion, references) used to look for `.def` files only next to the open file, silently failing for every other import. Modules are now searched in order: the importing file's directory, the project's `definitionDirectories`/`sourceDirectories`/ `libraryDirectories` from the nearest `modula2.json`, then gm2's own library sources (`m2iso` first, matching the default `-fiso` build; a `-fpim`/`-flog`/`-fmin` compiler flag promotes its dialect). Widening the search exposed and fixed, all probed against gm2: - definition-level `EXPORT` lists are captured, so re-exported foreign functions (`EXPORT UNQUALIFIED` in `FOR "C"` libraries) and gm2's own `SYSTEM.def` list count as exported; - `DEFINITION MODULE FOR "C" name` (name after the language tag) parses; - lone `__BUILTIN__` procedure attributes (`PROCEDURE __BUILTIN__ sin ...`) parse; - qualified formal types keep their dot (`ARRAY OF SYSTEM.LOC`, which the parameter parser used to record as `SYSTEM LOC`); - an open `ARRAY OF LOC`/`BYTE`/`WORD` is a raw-memory formal taking any actual (`RawIO.Read` into a `CHAR`); - `ADDRESS` takes `NIL` and mixes with pointers both ways, including `= NIL` comparisons; - field lookup falls back to the stamped declaration identity, reaching types that are not imported under their own name (`tm` behind `ptrToTM = POINTER TO tm`). ## 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. ## WITH-block internals `WITH` bodies used to be skipped wholesale; they are now fully checked with the guard record's fields in scope. The guard must denote a record variable or field (gm2 rejects anything else, including an undereferenced pointer and even a record-valued function result, which is a syntax error there); field names shadow outer scope, nesting works innermost-first, and bodies see real field types, so assignments, calls and expressions inside are checked exactly as qualified accesses would be. Genuinely unknown names are reported (`WITH rec DO nope := 1` and `WITH nope DO` both error, matching gm2). A 22-case differential suite passes with no mismatches, including shadowing, nesting, two-level access (`inner.y`), field guards (`WITH o.inner DO`), and fields named like builtins (`ORD`). Hover, go-to-definition, references and rename work through the same scopes (`resolveNameWith`): bare field names — and qualified heads like `inner` in `inner.y` — resolve to the guard record's declarations, innermost guard first, with nested guards resolving through outer blocks. Unknown names and unknowable guards stay silent, and reference search matches field declarations by position (they never resolve by name) without letting shadowing declarations leak across symbols. ## 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.