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,
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.
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.
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.