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:
(* ... *) comments;; after the module header;END <ModuleName>.;END;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.
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:
WITH regions, unverifiable FROM-imports, predefined
identifiers, closing END Name;(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.
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:
REAL↔LONGREAL,
CHAR/string rules with ARRAY OF CHAR length checks, sets, pointers,
enumerations, NIL;BOOLEAN conditions (IF, WHILE, ELSIF, UNTIL), ordinal CASE
selectors and FOR variables/bounds/BY steps;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,
builtin-procedure signatures, record-field types, WITH-block internals.
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:
.def (FileIO.mod + FileIO.def);FORWARD procedure headings (not duplicates of the later definition);^/[] (f^.x, a[i].x) and flattened multi-index
(a[i, j] over nested arrays);0C is CHAR, 177B/0FFH whole);'\' is one character);SIZE(T), VAL(T, x), MAX(T),
SYSTEM.TSIZE(T)) and typed set constructors (BITSET{...});TYPE File;), local-module EXPORT visibility,
procedure variables (calls and P := Q assignment), the pervasive
LENGTH/ORDL, arithmetic that preserves CARDINAL (c + 1).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.
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.