Browse Source

language: strict declaration order (no forward type references)

Track A, first cut: follow Modula-2, which requires declaration before
use.  A type may be forward-referenced only as a POINTER's base type.

TypeIdent no longer creates a forward placeholder for an undeclared type
unless a new `allowFwdType` flag is set; it is set exactly while parsing
a POINTER's base (and cleared by ARRAY/SET/RECORD/PROCEDURE so it cannot
leak into a nested sub-type).  So module-level forward type/VAR
references, forward types inside a TYPE block, and forward SET OF <type>
are now a plain 201.

The compiler's own sources and runtime/stdlib do not rely on forward
types, so fixpoint stays byte-identical.  Suite: t_fwdtype/t_fwdset
became reject tests, t_ptrfwd locks the legal POINTER exception.

Corpus 398 -> 388, 0 crashes: the 10 dropped files (arrayptr, arrayptr6,
constructor1, constructor3, param3, record14/15/16.def, tinyset6/7) were
gm2 leniency.  Fixpoint OK, 3,843,693 bytes.

Forward references to module-level variables/procedures from a body (the
two-phase parse) remain -- a separate, larger item.
Eric Streit 9 hours ago
parent
commit
5fb4950e65

+ 2 - 2
README.md

@@ -47,8 +47,8 @@ docs/              STATUS.md, plan.md, plan-*.md, analysis_*.md, summary_*.md
 
 ## Status (2026-10-18)
 
-- Suite **233/233**; self-hosting **fixpoint OK** (byte-identical
-  stage2/stage3, 3,842,992 bytes); corpus **398/612 compile-OK,
+- Suite **234/234**; self-hosting **fixpoint OK** (byte-identical
+  stage2/stage3, 3,843,693 bytes); corpus **388/612 compile-OK,
   0 crashes** (`tools/v3-corpus`; the corpus is gm2's testsuite, so it
   mixes conforming Modula-2 with gm2 leniencies — see `docs/gaps/`).
 - Active branch **`resolve-check`** (holds the AST/`Lower`/parse

+ 3 - 2
compiler/run_tests.sh

@@ -72,7 +72,7 @@ expect_run t_ctor7.mod 42
 expect_run t_minmax.mod 42
 expect_run t_minmax2.mod 42
 expect_run t_setops.mod 42
-expect_run t_fwdtype.mod 42
+expect_run t_ptrfwd.mod 42
 expect_run t_libc.mod 42
 expect_run t_variant2.mod 42
 expect_run t_addrptr.mod 42
@@ -94,7 +94,6 @@ expect_run t_long.mod 42
 expect_run t_truncfloat.mod 42
 expect_run t_enumidx.mod 42
 expect_run t_setenum.mod 42
-expect_run t_fwdset.mod 42
 expect_run t_setconst.mod 42
 expect_run t_constbuiltin.mod 42
 expect_run t_constreal.mod 42
@@ -176,6 +175,8 @@ expect_fail t_bad_for.mod "FOR needs integer variable and bounds"
 expect_fail t_bad_mixed.mod "incompatible assignment"
 expect_fail t_bad_fwd.mod "incompatible assignment"
 expect_fail t_bad_nestfwd.mod "undeclared identifier"
+expect_fail t_fwdtype.mod "undeclared identifier"
+expect_fail t_fwdset.mod "undeclared identifier"
 expect_fail t_bad_overlong.mod "invalid UTF-8"
 expect_fail t_bad_surrogate.mod "invalid UTF-8"
 expect_fail t_bad_utf8big.mod "invalid UTF-8"

+ 26 - 10
compiler/src/M2.atg

@@ -81,6 +81,10 @@ VAR
   deferredBody: BOOLEAN;
   repassing: BOOLEAN;
   allowDefer: BOOLEAN;
+  (* TRUE only while parsing a POINTER's base type: the single position
+     where Modula-2 allows a forward type reference.  Everywhere else an
+     undeclared type is a plain 201 (declaration before use). *)
+  allowFwdType: BOOLEAN;
   curProcNode: AST.Node;
 
   (* The last actual argument parsed by ActParam: folded value and type.
@@ -760,6 +764,7 @@ PRODUCTIONS
   Unit
     =                                   (. isGen := FALSE;
                                            allowDefer := FALSE;
+                                           allowFwdType := FALSE;
                                            repassing := FALSE; .)
       [ "GENERIC"                       (. isGen := TRUE; .) ]
       ( DefUnit | ImplUnit | ProgModule ) .
@@ -1216,12 +1221,15 @@ PRODUCTIONS
     | PointerType<t>
     | ProcType<t> .
   PointerType<VAR t: SymTab.TypeIndex>  (. VAR base: SymTab.TypeIndex; .)
-    = "POINTER" "TO" Type<base, FALSE>(. t := SymTab.NewPtr(base); .) .
+    = "POINTER" "TO"                    (. allowFwdType := TRUE; .)
+      Type<base, FALSE>                 (. allowFwdType := FALSE;
+                                           t := SymTab.NewPtr(base); .) .
   (* Procedure types (step 8.5): PROCEDURE (params): result. Values
      are code pointers; params are collected into the descriptor. *)
   ProcType<VAR t: SymTab.TypeIndex>     (. VAR res, pt: SymTab.TypeIndex;
                                              isV: BOOLEAN; .)
-    = "PROCEDURE"                       (. res := SymTab.InvalidType;
+    = "PROCEDURE"                       (. allowFwdType := FALSE;
+                                           res := SymTab.InvalidType;
                                            t := SymTab.NewProcType(res); .)
       [ "(" [ ProcTypeSection<t> { ";" ProcTypeSection<t> } ] ")" ]
       [ ":" TypeIdent<res>              (. SymTab.SetProcTypeRes(t, res); .) ] .
@@ -1268,7 +1276,7 @@ PRODUCTIONS
                                              nb, k: CARDINAL;
                                              idx: SymTab.TypeIndex;
                                              ilo, ihi: INTEGER; .)
-    = "ARRAY"
+    = "ARRAY"                           (. allowFwdType := FALSE; .)
       ( "OF" Type<elem, FALSE>          (. IF NOT allowOpen THEN
                                              SemError(230) END;
                                            t := SymTab.NewOpenArray(elem); .)
@@ -1342,14 +1350,14 @@ PRODUCTIONS
   SetType<VAR t: SymTab.TypeIndex>      (. VAR base: SymTab.TypeIndex;
                                              blo, bhi, bspan: INTEGER;
                                              bcls: INTEGER; .)
-    = "SET" "OF" Type<base, FALSE>
+    = "SET"                             (. allowFwdType := FALSE; .)
+      "OF" Type<base, FALSE>
                                         (. IF base = SymTab.InvalidType THEN
                                              t := SymTab.InvalidType
                                            ELSIF SymTab.Unresolved(base) THEN
-                                             (* a forward type: defer the
-                                                span check; the base
-                                                resolves once the TYPE
-                                                block completes *)
+                                             (* an opaque/formal base: defer
+                                                the span check until the
+                                                base is completed *)
                                              t := SymTab.NewSet(base)
                                            ELSE bcls :=
                                                   SymTab.ClassOf(base);
@@ -1381,7 +1389,8 @@ PRODUCTIONS
      (which starts with an identifier). *)
   RecordType<VAR t: SymTab.TypeIndex>   (. VAR t2: SymTab.TypeIndex;
                                              tagOk: BOOLEAN; .)
-    = "RECORD"                          (. t := SymTab.NewRecord(); .)
+    = "RECORD"                          (. allowFwdType := FALSE;
+                                           t := SymTab.NewRecord(); .)
       [ RecItem<t> { ";" [ RecItem<t> ] } ]
       "END"                             (. SymTab.LayoutRecord(t); .) .
   RecItem<rec: SymTab.TypeIndex>        (. VAR tt: SymTab.TypeIndex; .)
@@ -1438,8 +1447,15 @@ PRODUCTIONS
                                              dotted: BOOLEAN; .)
     = GetIdent<n>                       (. dotted := FALSE;
                                            IF NOT SymTab.Lookup(n) THEN
+                                             (* declaration before use: an
+                                                undeclared type is 201,
+                                                except as a POINTER's base
+                                                (allowFwdType) *)
                                              k := -1;
-                                             t := SymTab.ForwardType(n);
+                                             IF allowFwdType THEN
+                                               t := SymTab.ForwardType(n)
+                                             ELSE t := SymTab.InvalidType
+                                             END;
                                              IF t = SymTab.InvalidType THEN
                                                SemError(201)
                                              END

File diff suppressed because it is too large
+ 3981 - 3965
compiler/src/M2.lst


+ 3 - 3
compiler/tests/t_fwdset.mod

@@ -1,7 +1,7 @@
 MODULE TFwdSet;
-// SET OF a type declared later in the same TYPE block (forward type
-// reference): the span check is deferred until the block completes.
-// Exit 42.
+// REJECTED BY DESIGN: SET OF a type declared later in the same TYPE
+// block (a forward type reference).  Modula-2 requires declaration
+// before use.  Expects "undeclared identifier".
 TYPE myset = SET OF colour;
      colour = (red, green, blue);
 VAR s : myset; ExitCode : INTEGER;

+ 4 - 2
compiler/tests/t_fwdtype.mod

@@ -1,6 +1,8 @@
 MODULE tfwdtype;
-// Module-level forward type reference: a global whose type is declared
-// further down (emission deferred until the TYPE completes).
+// REJECTED BY DESIGN: a module-level forward type reference (a global
+// whose type is declared further down).  Modula-2 requires declaration
+// before use; only a POINTER's base type may forward-reference.  gm2
+// accepts this as a leniency.  Expects "undeclared identifier".
 VAR g : Later;
 TYPE Later = RECORD x, y : INTEGER END;
 VAR ExitCode : INTEGER;

+ 12 - 0
compiler/tests/t_ptrfwd.mod

@@ -0,0 +1,12 @@
+MODULE TPtrFwd;
+// LEGAL forward reference: a POINTER's base type may be declared later
+// (for recursive / self-referential structures).  Exit 42.
+TYPE Node = POINTER TO NodeRec;
+     NodeRec = RECORD next : Node; v : INTEGER END;
+VAR p : Node;
+    ExitCode : INTEGER;
+BEGIN
+  NEW(p);
+  p^.v := 42;
+  IF p^.v = 42 THEN ExitCode := 42 ELSE ExitCode := 1 END
+END TPtrFwd.

+ 12 - 10
docs/STATUS.md

@@ -4,10 +4,10 @@ This answers "where are we now?".
 
 ## Numbers
 
-- Suite **233/233** (`compiler/run_tests.sh`).
+- Suite **234/234** (`compiler/run_tests.sh`).
 - Self-hosting **FIXPOINT OK** — stage2 and stage3 images are
-  byte-identical (3,842,992 bytes).
-- Corpus **398 / 612** sources compile with 0 errors, **0 crashes**
+  byte-identical (3,843,693 bytes).
+- Corpus **388 / 612** sources compile with 0 errors, **0 crashes**
   (`tools/v3-corpus/corpus.sh` over the gm2 testsuite); the rest are
   genuine language gaps, **gm2 leniencies V3 deliberately rejects**, or
   unresolved imports.
@@ -85,12 +85,14 @@ corpus is gcc's gm2 testsuite, so its number mixes conforming programs
 with gm2 leniencies; each known case is catalogued (with a reproducer)
 in `docs/gaps/`.
 
-*Known non-conforming behaviour still retained* (pre-existing, not part
-of this policy change yet): module-level forward type/`VAR` references,
-forward type references inside a `TYPE` block, forward `SET OF <type>`,
-and the two-phase parse's forward **procedure** calls without `FORWARD`.
-These are deviations from strict declaration-before-use; removing them
-is a larger change.
+*Known non-conforming behaviour still retained* (pre-existing, not yet
+removed): forward references to module-level **variables** and forward
+**procedure** calls from a body (both are the two-phase parse deferring
+procedure bodies — separate from the declaration part).  The declaration
+part itself is now strict: module-level forward type/`VAR` references,
+forward types inside a `TYPE` block, and forward `SET OF <type>` are all
+rejected (Track A, first cut, 2026-10-18).  Corpus 398 → 388 (the drop
+is the 10 gm2-leniency files now correctly rejected).
 
 ## What remains
 
@@ -112,7 +114,7 @@ is a larger change.
 ```sh
 cd compiler
 ./build.sh          # M2.atg -> scanner/parser/driver -> ./M2
-./run_tests.sh      # 233/233
+./run_tests.sh      # 234/234
 cd ..
 ./bootstrap/fixpoint.sh   # FIXPOINT OK (byte-identical stage2/stage3)
 ```

+ 13 - 0
docs/analysis_gaps.md

@@ -240,6 +240,19 @@ 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`

+ 5 - 1
docs/gaps/README.md

@@ -45,7 +45,7 @@ The `class` column says what a change would mean:
 | tag | file | class | diagnostic | corpus exemplars |
 | --- | --- | --- | --- | --- |
 | `gap01-fwd-type-const-conv` | `gap01_fwd_type_const_conv.mod` | `decl-order` | `undeclared identifier` | `subrange4`, `subrange7` |
-| `gap02-fwd-type-var` | `gap02_fwd_type_var.mod` | `decl-order` | `undeclared identifier` | `subrange11` (proc-local) |
+| `gap02-fwd-type-var` | `gap02_fwd_type_var.mod` | `decl-order` | `undeclared identifier` | `subrange11`, `arrayptr`, `arrayptr6`, `constructor1`, `constructor3`, `param3` |
 | `gap03-fwd-type-heading` | `gap03_fwd_type_heading.mod` | `decl-order` | `undeclared identifier` | `shift3`, `shift4` |
 | `gap04-fwd-const-bound` | `gap04_fwd_const_bound.mod` | `decl-order` | `undeclared identifier` | `setconst3` |
 | `gap05-fwd-type-size` | `gap05_fwd_type_size.mod` | `decl-order` | `undeclared identifier` | `constsize2` |
@@ -64,6 +64,8 @@ The `class` column says what a change would mean:
 | `gap17-set-of-cardinal` | `gap17_set_of_cardinal.mod` | `impl-limit` | `not supported yet` | `settest3` |
 | `gap18-set-case-labels` | `gap18_set_case_labels.mod` | `gm2-ext` | `not supported yet` | `bitset5` |
 | `gap19-fwd-array-index` | `gap19_fwd_array_index.mod` | `decl-order` | `not supported yet` | `record7`, `complextypes` |
+| `gap20-fwd-set-of-type` | `gap20_fwd_set_of_type.mod` | `decl-order` | `undeclared identifier` | `tinyset6`, `tinyset7` |
+| `gap21-fwd-record-field` | `gap21_fwd_record_field.mod` | `decl-order` | `undeclared identifier` | `record14.def`, `record15.def`, `record16.def` |
 
 First diagnostic (message) per file at commit time (line:col varies as
 comments change):
@@ -89,6 +91,8 @@ gap16_wide_set.mod              not supported yet       (span > 65536)
 gap17_set_of_cardinal.mod       not supported yet       (unbounded base)
 gap18_set_case_labels.mod       not supported yet       (set as CASE selector)
 gap19_fwd_array_index.mod       not supported yet       (array index declared later)
+gap20_fwd_set_of_type.mod       undeclared identifier   (SET OF base declared later)
+gap21_fwd_record_field.mod      undeclared identifier   (record field type declared later)
 ```
 
 ## Genuine gaps (worth fixing, standard Modula-2)

+ 2 - 1
docs/gaps/gap02_fwd_type_var.mod

@@ -2,7 +2,8 @@ MODULE Gap02FwdTypeVar;
 (* NOT legal Modula-2: a procedure-local VAR names `arr` before its
    local TYPE declaration.  Declaration before use is required; V3
    correctly rejects it.  Class: decl-order (gm2 leniency).
-   Corpus: subrange11 (procedure-local part).
+   Corpus: subrange11; module-level VAR forwards: arrayptr,
+   arrayptr6, constructor1, constructor3, param3.
    V3: undeclared identifier. *)
 PROCEDURE outer;
 VAR a : arr;

+ 10 - 0
docs/gaps/gap20_fwd_set_of_type.mod

@@ -0,0 +1,10 @@
+MODULE Gap20FwdSetOfType;
+(* NOT legal Modula-2: SET OF a type declared later in the same TYPE
+   block.  Declaration before use is required; V3 correctly rejects it.
+   Class: decl-order (gm2 leniency).
+   Corpus: tinyset6, tinyset7 (and the suite's old t_fwdset).
+   V3: undeclared identifier. *)
+TYPE myset = SET OF colour;
+     colour = (red, green, blue);
+VAR s : myset;
+BEGIN END Gap20FwdSetOfType.

+ 11 - 0
docs/gaps/gap21_fwd_record_field.mod

@@ -0,0 +1,11 @@
+MODULE Gap21FwdRecordField;
+(* NOT legal Modula-2: a record (or variant) field's type is declared
+   later; only a POINTER's base type may forward-reference.  Declaration
+   before use is required; V3 correctly rejects it.
+   Class: decl-order (gm2 leniency).
+   Corpus: record14.def, record15.def, record16.def.
+   V3: undeclared identifier. *)
+TYPE r = RECORD f : s END;
+     s = RECORD v : INTEGER END;
+VAR x : r;
+BEGIN END Gap21FwdRecordField.

+ 24 - 7
docs/summary_set-constants-2026-10-18.md

@@ -1,11 +1,12 @@
 # Session summary — SET CONST + builtin CONST folding 2026-10-18
 
-Branch **`resolve-check`**.  Suite **233/233**; fixpoint **OK
-(3,842,992 bytes)**; corpus **398/612 compile-OK, 0 crashes**.
+Branch **`resolve-check`**.  Suite **234/234**; fixpoint **OK
+(3,843,693 bytes)**; corpus **388/612 compile-OK, 0 crashes**.
 
 Tags: `v3-set-constants`, `v3-const-builtin-fold`, `v3-const-conv-fold`,
 `v3-proc-addr`, `v3-proc-val`, `v3-real64`, `v3-shift-import`,
-`v3-foreign-module`, `v3-gap-examples`.
+`v3-foreign-module`, `v3-m2-conformance`, `v3-strict-decl-order`,
+`v3-gap-examples`.
 
 ## Conformance note
 
@@ -24,7 +25,7 @@ previous corpus sweep — **set constants** (static masks) and **builtin
 calls in `CONST` initialisers** — then went further on `CONST`
 initialisers, procedure values, SYSTEM sized reals, builtin imports, and
 ISO foreign modules.  (A forward-type-reference change was added and then
-reverted for conformance; see the note at the top.)  Corpus **382 → 398**
+reverted for conformance; see the note at the top.)  Corpus **382 → 388**
 (+16 files, 0 crashes).
 
 ```mod
@@ -139,6 +140,21 @@ suite test `t_shiftneg`.
 (ISO foreign-module syntax) are now parsed; `v.def` → 0 errors.  Corpus
 **399 → 400**; suite session test `CForeignProg`.
 
+## Conformance: strict declaration order (Track A, first cut)
+
+`TypeIdent` no longer invents a forward placeholder for an undeclared
+type: a new `allowFwdType` flag is set **only** while parsing a
+`POINTER`'s base type (Modula-2's single forward-reference exception), so
+module-level forward type/`VAR` references, forward types inside a
+`TYPE` block, and forward `SET OF <type>` are now `201`.  The compiler's
+own sources (and runtime/stdlib) do not rely on it — fixpoint stays
+green.  `t_fwdtype`/`t_fwdset` became reject tests; `t_ptrfwd` locks the
+legal exception.  Corpus **398 → 388**: the 10 files dropped
+(`arrayptr`, `arrayptr6`, `constructor1`, `constructor3`, `param3`,
+`record14.def`–`record16.def`, `tinyset6`, `tinyset7`) were gm2
+leniency.  Forward references to module-level **variables**/**procedures**
+from a body (the two-phase parse) remain — a separate, larger item.
+
 ## Not fixed (recorded)
 
 - **`setconst3.mod`** still fails: its `VAR mem: ARRAY [0..CARDINAL(
@@ -168,7 +184,8 @@ modules),
 `compiler/tests/t_setconst.mod`, `t_bad_setconst.mod`,
 `t_constbuiltin.mod`, `t_constreal.mod`, `t_constlong.mod`,
 `t_procaddr.mod`, `t_procval.mod`, `t_real64.mod`,
-`t_shiftneg.mod`, `c_foreign.def`, `c_foreign.mod`, `c_foreign_prog.mod`,
+`t_shiftneg.mod`, `t_ptrfwd.mod`, `t_fwdtype.mod`, `t_fwdset.mod`,
+`c_foreign.def`, `c_foreign.mod`, `c_foreign_prog.mod`,
 `compiler/run_tests.sh`.
 
 ## Resume
@@ -176,7 +193,7 @@ modules),
 ```sh
 cd .../m2compiler-V3
 git checkout resolve-check
-cd compiler && ./build.sh && ./run_tests.sh   # 233/233
+cd compiler && ./build.sh && ./run_tests.sh   # 234/234
 cd .. && TMP="$PWD/.scratch/fixpoint" ./bootstrap/fixpoint.sh
-M2FLAGS="" ./.scratch/corpusx.sh              # 398 OK, 0 crash
+M2FLAGS="" ./.scratch/corpusx.sh              # 388 OK, 0 crash
 ```

Some files were not shown because too many files changed in this diff