Historical (2026-09-30). Current state: suite 184/184, FIXPOINT OK (3,392,601 bytes); there is now a full AST and a two-phase
Loweremitter (docs/STATUS.md). The list below is kept as the gap analysis from that date; several items have since landed (§7).
Suite 169/169, self-hosting fixpoint OK (2,754,353 bytes).
Corpus at this point (with $V3_LIBS):
| corpus | compile-OK | CRASH | LANG |
|---|---|---|---|
| gm2 testsuite (612) | 392 | 0 | 212 |
| gm2 stdlib (336) | 127 | 0 | 209 |
Failure taxonomy (testsuite, per file): undeclared identifier 65,
not supported yet 43, invalid call 25, incompatible assignment
25, ident expected 8, invalid ImpName 7.
FROM libc intrinsic — printf/exit/… import as permissive
external C procedures (varargs, no result) and bind to the C
symbols; string/CHAR-array actuals pass as descriptor+8. This
removed the dominant undeclared bucket (136 → 65 files).SET OF enum and SET OF <char-range> (plus enum subranges
[a..d], ARRAY [FALSE..TRUE]), and the set span limit lifted
256 → 65536.MIN/MAX for REAL/LONGREAL/LONGINT and gm2 sized names
(LONGCARD, SHORTREAL, INTEGER8/16/32/64, CARDINAL8/…).INCL/EXCL (statements) and
SHIFT/ROTATE (expressions), bit-by-bit over a runtime amount.Ranked by corpus impact (each verified by a failing file or a probe):
SET OF <enum> and
SET OF ['A'..'z'] are 230. Only bool/char/integer subrange
bases are accepted. (goodifin.mod, charset2.mod, enum3.mod.)MIN/MAX for REAL/LONGREAL/LONGINT — only ordinal types,
arrays and sets are handled; MAX(REAL) / MIN(LONGINT) are 230.
(maxreal.mod, minmaxreal2.mod, longmm.mod, constcast.mod.)LONGCARD, SHORTREAL, INTEGER8/16/32/64,
CARDINAL8/16/32/64, LONGWORD undeclared.SHIFT, INCL, EXCL, ROTATE, LOC,
sized-bit names (INCL/EXCL/SHIFT recur).[a..d],
ARRAY [FALSE..TRUE] … are 230.String — not a gm2 built-in: it is an opaque type imported
from a definition module (DynamicStrings, or a local opaque).
V3 now treats unresolved (opaque/forward) aliases as pointer-like,
which is the real fix.GOTO + labels — parse error.236.MODULE — parsed, not lowered.F()^.docs/analysis_generics.md).NEWPROCESS/TRANSFER/IOTRANSFER).docs/OOP.txt).DISPOSE (shallow today) and runtime/syslib/Trap
(traps are $abort, rc 134).Procedure types and indirect calls do work (t_proctype passes);
docs/features.md still lists them as ⏸ — stale.
String, StrIO, RealStr/WholeStr,
LongIO/LongMath/LongStr, the InOut/StdIO/STextIO/
SWholeIO/SIOResult family, IOConsts, Termination,
StdChans/StreamFile/FIO, SysStorage, wrapclock/time,
ChanConsts/GenDevIF/DeviceTablePtr (the gm2 device/IO framework).FROM libc IMPORT printf, exit — no intrinsic libc module;
this is the largest undeclared identifier bucket (163 + 141
occurrences) and is not a real language gap.Lower emits from it (docs/STATUS.md).
Declaration-before-use is still enforced by the grammar's semantic
pass, so forward references in CONST/expressions still fail (only
FORWARD procedures and ForwardType are tolerated).M2 exits 0 even with errors (harnesses parse
the .LST verdict) and aborts on a missing input file.ident
expected / 'END' expected.Classic identifiers (no _), # not <>, no octal B/C, 0x-only
hex, NOT/AND/OR only, TopSpeed legacy isolated in the sidecar.
FROM libc intrinsicSET OF enum + SET OF char-rangeMIN/MAX for REAL/LONGINT + gm2 sized typesINCL/EXCL/SHIFT/ROTATEAlso done: opaque/pointer-like types (incl. String), variant-record
assignment (flat blit), nested-MODULE lowering, ADDRESS↔pointer
assignment.
Remaining after today: generics, multiple inheritance (deliberate), and
the channel/device library holes in §2.
(Statement-context result suffixes F()^.x := e, composite function
results — record/array/set/class — chained post-call method calls
F().M(), and GOTO/labels all landed 2026-10-08.)
F()^, F()[i], F().field (expression
context). Statement-context suffix targets (F()[i] := x) and
chained post-call calls (F().M()) are still not accepted.PROCESS + NEWPROCESS/TRANSFER/IOTRANSFER
(ucontext-backed).ConvTypes, IOConsts, SIOResult,
TERMINATION, StrIO, InOut, StdIO, WholeStr, RealStr,
LongStr, LongMath, LongIO, STextIO, SWholeIO, SRealIO,
DynamicStrings (core), SysStorage.tools/v3-corpus/corpus.sh over the gm2 testsuite (612 files), current
compiler: 365 compile-OK, 0 CRASH, -notwophase 358 (the
two-phase parse gains 7, loses none). Top LANG buckets: not
supported yet 36, undeclared identifier 35, invalid call 28.
Two concrete, corpus-visible language gaps found while auditing those
T(x) — a type name applied to one value,
e.g. INTEGER(c), CARDINAL(i), ind1(-200) (a subrange/enum
conversion). DONE (2026-10-16). ArgList now accepts a type
callee (KindType or KindPredef) as a one-argument conversion and
Fact emits it as the existing VAL(T, x) builtin, so Lower's
conversion rules apply unchanged. Suite test t_conv; corpus
365 -> 368 (+3 files: procedure1, tinyindr, tinyproc6),
invalid call 28 -> 17. CONST initialisers (CONST low =
ind1(-200)) are folded too (2026-10-16): a conversion whose operand
is an immediate ordinal and whose source and target classes are
ordinal (OrdClass) folds to the operand. Corpus 368 -> 372
(subrange3/9/10/12 now pass); suite test t_convconst.
Conversions that genuinely change representation (int<->real,
long<->int) are not folded, so CONST r = REAL(intconst) is still
230.SIZE(TYPE) / TSIZE(TYPE) as expressions — sizes.mod.
Not a gap: already supported (grammar Fact: ("SIZE"|"TSIZE")
"(" Design ")", lowered by LowerBuiltinExpr). Keep the layout
caveat in mind if extending it.Neither is started; both are bounded and corpus-measurable.
Landed (each with a suite test, corpus measured):
ODD/TRUNC/FLOAT builtins (+4), BOOLEAN/ENUM array bounds +
indices (+5), SET OF <enum> literals (+0), forward type
reference in SET OF <type> (+1). Corpus 372 -> 382, 0 crashes.
Investigating further surfaced two gaps that are features, not
one-liners (deliberately left as 230, no silent miscompile):
CONST Digits = CharSet{"0".."9"}, setconst*.mod).
DONE (2026-10-18). The grammar folds the literal/union to a
bit mask (Lower.SetConstMaskOf, up to 512 bits) and records it
(Lower.NoteSetMask); Lower re-emits it as a pooled static
descriptor (QbeGen.PoolSet → data $setN = { w … }), and a ClSet
KindConst is an aggregate (its value is the descriptor address).
Set unions/differences of constants fold too. Corpus 382 -> 387;
suite tests t_setconst/t_bad_setconst. setconst3 still fails
on its forward CONST reference in a VAR bound (separate gap).CONST (CONST A = CAP(VAL(CHAR,
65)), testcap2.mod; MIN/MAX of sets). DONE (2026-10-18).
CAP/ABS/ODD now fold over an immediate operand in the grammar's
Fact (so ConstDecl's IsImm check accepts them); MIN/MAX of a
set already folded via TypeBounds. A BOOLEAN CONST other than
TRUE/FALSE was also rejected at the designator — the grammar's
Design class list was missing ClBool. Corpus 387 -> 389;
suite test t_constbuiltin. constcast (REAL<->CARDINAL CONST
conversions) and minmaxreal3 (SYSTEM REAL64) remain.Both recorded features are now closed. Pushing further on CONST
initialisers also landed representation-changing conversions:
VAL(REAL, intimm) / FLOAT (constcast), VAL(LONGCARD, longimm)
(maxlongint), INT↔LONGINT and LONGINT→LONGINT fold on an
immediate. Corpus 389 -> 391; suite tests t_constreal,
t_constlong.
Forward array-index types — added then REVERTED. TYPE a = ARRAY
idx OF T; idx = [-2..2] (record7, complextypes) was briefly
accepted by deferring the array bounds (SymTab.DeferArrayIdx/
FixupArrayIdxs, tFwd). This is use-before-declaration, forbidden
by Modula-2 (only a POINTER's base type may forward-reference), so it
was reverted and V3 rejects those files again. See the conformance
policy below.
Also landed procedure addresses: Lower's ADR now yields the code
address for a procedure (was 0), and the VAL conversion accepts
PROC↔PROC/PTR; procadr passes and runs. Assignable also now
takes a procedure value into a PROC/pointer (p := foo), so
procconv lands too. Corpus 393 -> 396; suite tests t_procaddr,
t_procval.
REAL32/REAL64 (SYSTEM sized reals) are now predefined (mapped onto
REAL), so MIN(REAL64)/MAX(REAL64) fold; minmaxreal3 passes.
Corpus 396 -> 397; suite test t_real64.
FROM SYSTEM IMPORT SHIFT/ROTATE/CAST/… now parse (ImpName
accepts builtin tokens; the import is ignored). This exposed a real
miscompile: SHIFT by an out-of-range amount addressed a wild word
and loaded it. QbeGen.SetShift now clamps the word index to 0 when the
destination bit is out of range. Corpus 397 -> 399 (shift,
shift2); suite test t_shiftneg.
ISO foreign modules DEFINITION/IMPLEMENTATION MODULE FOR "C" M and
EXPORT UNQUALIFIED are now parsed; v.def → 0 errors. Corpus
399 -> 400 (the array-index revert then brings the measured total to
398).
Conformance policy (2026-10-18): V3 follows Modula-2, not gm2.
Declaration before use is enforced (only a POINTER base may forward-
reference), so gm2's leniencies are rejected by design. docs/gaps/
catalogues every known remaining case with a reproducer and a class
(decl-order / gm2-ext = not legal M-2; iso/system = genuine gap;
v3-deviation = V3 bug; impl-limit).
Track A, first cut (2026-10-18): the declaration part is now strict.
TypeIdent no longer creates a forward placeholder except as a
POINTER's base (a new allowFwdType flag, set only by PointerType),
so module-level forward type/VAR references, forward types inside a
TYPE block, and forward SET OF <type> are all rejected. Corpus
398 -> 388 (the drop is 10 gm2-leniency files: arrayptr,
arrayptr6, constructor1, constructor3, param3, record14.def,
record15.def, record16.def, tinyset6, tinyset7); suite
t_fwdtype/t_fwdset turned into reject tests and t_ptrfwd locks the
legal pointer exception; fixpoint stays green. Forward references to
module-level variables / procedures from a body (the two-phase
parse) are a separate, larger item still retained.
Track B (genuine gaps), 2026-10-18: CONVERT(T, x) (ISO SYSTEM)
landed — the VAL production now accepts ( "VAL" | "CONVERT" ), so
procconv2 passes and runs. Then set↔word VAL/CAST (the VAL
production also takes CAST): set→CARDINAL loads word 0, and
CARDINAL→set builds a zeroed one-word set temp; constset,
constset2, constset3 pass. Corpus 388 -> 392; suite tests
t_convert, t_setword; docs/gaps/gap07/gap08 retired.
Next (recorded, genuine only): gap13 builtins as identifiers (a V3
bug; needs de-reserving the builtin keyword tokens — a parser refactor);
gap15 enumeration literals with an imported type (imp*);
aochar/arrayofbyte/bytearray (scalar actual into ARRAY OF BYTE);
packed (__FILE__/__LINE__, gm2-ext); variant empty arms / ELSE;
set-span limits. The forward-type items above are gm2 leniency, not
targets.
Every gap above has a self-contained reproducer in docs/gaps/
(docs/gaps/README.md indexes them by tag, with a class:
decl-order = gm2 accepting use-before-declaration, which strict
Modula-2 forbids and V3 correctly rejects; iso/system; gm2-ext;
v3-deviation; impl-limit). In particular the forward type/const
cases (subrange4/7/11, shift3/4, constsize2, setoverflow,
setconst3) are not standard Modula-2 — only a POINTER's base type
may be declared later.