analysis_gaps.md 14 KB

m2compiler-V3 — gap analysis (2026-09-30)

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 Lower emitter (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.

6. Fixed today (items 1–5)

  1. 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).
  2. SET OF enum and SET OF <char-range> (plus enum subranges [a..d], ARRAY [FALSE..TRUE]), and the set span limit lifted 256 → 65536.
  3. MIN/MAX for REAL/LONGREAL/LONGINT and gm2 sized names (LONGCARD, SHORTREAL, INTEGER8/16/32/64, CARDINAL8/…).
  4. SYSTEM/PIM set builtins — INCL/EXCL (statements) and SHIFT/ROTATE (expressions), bit-by-bit over a runtime amount.
  5. Module-level forward type references — a global may name a type declared further down; its emission is deferred until the TYPE block completes (TYPE-block forwards already worked).

1. Language features genuinely missing

Ranked by corpus impact (each verified by a failing file or a probe):

  1. Sets over enumerations and CHAR ranges — SET OF <enum> and SET OF ['A'..'z'] are 230. Only bool/char/integer subrange bases are accepted. (goodifin.mod, charset2.mod, enum3.mod.)
  2. 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.)
  3. gm2 sized types — LONGCARD, SHORTREAL, INTEGER8/16/32/64, CARDINAL8/16/32/64, LONGWORD undeclared.
  4. SYSTEM builtins — SHIFT, INCL, EXCL, ROTATE, LOC, sized-bit names (INCL/EXCL/SHIFT recur).
  5. Enum subranges / BOOLEAN bounds — [a..d], ARRAY [FALSE..TRUE] … are 230.
  6. 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.
  7. GOTO + labels — parse error.
  8. Whole-variant-record assignment/equality — deliberately 236.
  9. Nested local MODULE — parsed, not lowered.
  10. Result suffixes F()^.
  11. Generics — not started (docs/analysis_generics.md).
  12. Coroutines / processes (NEWPROCESS/TRANSFER/IOTRANSFER).
  13. Multiple inheritance — deliberately out of scope (docs/OOP.txt).
  14. Recursive 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.

2. Library

  • gm2-only modules V3 lacks: 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.

3. Architecture / robustness

  • No AST, single pass. Superseded (2026-10-08): the grammar now builds a full AST and 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).
  • Single image only — no per-module object emission, no import cycles.
  • Driver robustness — M2 exits 0 even with errors (harnesses parse the .LST verdict) and aborts on a missing input file.
  • Fixed-size tables — 64 formals/locals (233 past that), 64 call args, 256 VAR-actual note entries, 4096 string literals, 256 pooled constructors, 8 constructor levels, 4 MiB image buffer.
  • Weak error recovery — one bad token cascades into ident expected / 'END' expected.

4. Deliberate dialect choices (not gaps)

Classic identifiers (no _), # not <>, no octal B/C, 0x-only hex, NOT/AND/OR only, TopSpeed legacy isolated in the sidecar.

5. Biggest wins (all five landed today — see §6)

  1. FROM libc intrinsic ✅
  2. SET OF enum + SET OF char-range ✅
  3. MIN/MAX for REAL/LONGINT + gm2 sized types ✅
  4. SYSTEM INCL/EXCL/SHIFT/ROTATE ✅
  5. Forward type references ✅ (module-level globals; TYPE-block forwards already worked)

Also 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.)

7. Done after the gap analysis (this session)

  • Result suffixes — 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.
  • PIM coroutines — PROCESS + NEWPROCESS/TRANSFER/IOTRANSFER (ucontext-backed).
  • Core text/ISO library — ConvTypes, IOConsts, SIOResult, TERMINATION, StrIO, InOut, StdIO, WholeStr, RealStr, LongStr, LongMath, LongIO, STextIO, SWholeIO, SRealIO, DynamicStrings (core), SysStorage.
  • Backend — procedure values / LONGINT are 8-byte in array load/ store and in the parameter prologue (were 32-bit); driver session file cap 32 → 128.

8. Corpus snapshot after the two-phase parse (2026-10-16)

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

  1. ISO type conversions 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.
  2. 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.

9. Corpus sweep 2026-10-18 (continuing §8)

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):

  1. Set constants (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).
  2. Builtin calls in 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.

Next (recorded, genuine only): forward type references in a CONST conversion (subrange4/7) and as a VAR type before its TYPE block (subrange11, shift3/4); setconst3 (forward CONST); procconv2 (CONVERT); packed (CAST set↔int); aochar/arrayofbyte/ bytearray (scalar actual into ARRAY OF BYTE); variant empty arms / ELSE; builtin names as procedure names; the imp* qualified-import family (partly a corpus-harness transitive-.def gap).

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.