Просмотр исходного кода

feat: emit separate generic-instance objects

Eric Streit 3 дней назад
Родитель
Сommit
a3664a2452

+ 2 - 2
README.md

@@ -88,12 +88,12 @@ docs/              STATUS.md, plan.md, plan-*.md, analysis_*.md, summary_*.md
 ## Status (2026-10-24)
 
 - Suite **305/305**; self-hosting **fixpoint OK** (byte-identical
-  stage2/stage3, 3,806,078 bytes); corpus **519/612 compile-OK,
+  stage2/stage3, 3,806,457 bytes); corpus **519/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/`).
   The remaining `except*`/`onebyte*`-style corpus files still hit V3's
   declaration-order policy.
-- Active branch **`master`**, feature state **`v3-separate-abi-vtables`**.
+- Active branch **`master`**, feature state **`v3-generic-instances`**.
   Recent milestones: `PACKEDSET`, ISO exceptions (`EXCEPT`/`RETRY`,
   `EXCEPTIONS`, `M2EXCEPTION`, catchable language traps including
   `functionException`, module `FINALLY`), self-referential pointer

+ 4 - 2
TUTORIAL.md

@@ -149,7 +149,7 @@ TMP="$PWD/.scratch/fixpoint-separate" ./bootstrap/fixpoint-separate.sh
 tools/v3-corpus/corpus.sh
 ```
 
-Current gates: suite **305/305**, fixpoint **OK (3,806,078 bytes)**,
+Current gates: suite **305/305**, fixpoint **OK (3,806,457 bytes)**,
 corpus **519/612 compile-OK, 0 crashes**.
 
 ## 7. Current limits
@@ -159,7 +159,9 @@ corpus **519/612 compile-OK, 0 crashes**.
   the shim, or the output changes, and treats a newer compiler binary as an
   ABI event.  A small content ABI/flags stamp (`gen_ssa/.m2abi`) forces
   a rebuild when the compiler ABI or `--m2flag` settings change.
-- Generic instance objects are not emitted separately yet.
+- Type/value/procedure generic instances are emitted as separate
+  objects by M2make; generic-class instances still use the client
+  implementation path.
 - Per-object self-hosting is a green gate via
   `bootstrap/fixpoint-separate.sh`; compiler units use
   `-legacyopen1d`, while user programs keep pointer views.

+ 2 - 0
compiler/src/compiler.frm

@@ -792,7 +792,9 @@ MODULE -->Grammar;
     SetTwoPhase(doTwoPhase);
     Parse;
     InstEnd;
+    IF separate THEN Lower.SetDefUnit(isDef) END;
     Lower.LowerUnitNow(GetUnit(), FALSE);
+    IF separate THEN Lower.SetDefUnit(FALSE) END;
     IF (src # FileIO.StdIn) & (src # FileIO.StdOut) THEN
       FileIO.Close(src)
     END;

+ 4 - 4
docs/STATUS.md

@@ -6,7 +6,7 @@ This answers "where are we now?".
 
 - Suite **305/305** (`compiler/run_tests.sh`).
 - Self-hosting **FIXPOINT OK** — stage2 and stage3 images are
-  byte-identical (3,806,078 bytes).
+  byte-identical (3,806,457 bytes).
 - Corpus **519 / 612** sources compile with 0 errors, **0 crashes**
   (`tools/v3-corpus/corpus.sh` over the gm2 testsuite; the harness now
   resolves V3's own shipped libraries); the rest are genuine language
@@ -14,7 +14,7 @@ This answers "where are we now?".
   imports of gm2-only modules.  Open-array corpus cases `unbounded`,
   `unbounded2`, `unbounded3`, `unbounded4` and `array9` now compile;
   `unbounded`/`unbounded2`/`unbounded3`/`unbounded4` also run.
-- Branch **`master`**, feature state **`v3-separate-abi-vtables`**; tree
+- Branch **`master`**, feature state **`v3-generic-instances`**; tree
   clean.
 
 ## Architecture (current)
@@ -202,8 +202,8 @@ declarations still work (`t_fwdproc`, `t_fwdok`).
    per-unit objects (libraries have no `main`), with stable module/class
    symbols, `export data`, and dependency-ordered init calls.  `M2make
    --separate` builds stale objects incrementally and links them.  Core
-   shipped; generic-instance objects remain.  Per-object self-hosting
-   now has its own green gate.  See
+   shipped; type/value/procedure generic instances are separate
+   objects.  Per-object self-hosting now has its own green gate.  See
    `docs/separate-compilation.md`;
    summary: `summary_session-2026-10-24-separate-compilation.md`.
 5. **Library folders** — `stdlib/` is retired; libraries live in

+ 4 - 4
docs/comparison-PIM4-gm2-V3.md

@@ -2,7 +2,7 @@
 
 **Date:** 2026-10-24 · **V3 state:** open arrays, dialect library
 folders and separate compilation with `M2 -I`/incremental objects
-(suite 305/305, self-hosting fixpoint OK 3,806,078 bytes, corpus
+(suite 305/305, self-hosting fixpoint OK 3,806,457 bytes, corpus
 519/612, 0 crashes).
 
 **Scope and sources.**
@@ -204,9 +204,9 @@ change individual cells.
   `<Module>__<Class>__vt`.  `M2 -I` resolves interfaces transitively;
   `M2make --separate` rebuilds stale objects and relinks on source/
   interface/compiler/shim changes.  The one-shot mode remains the
-  default for the suite/corpus.  Still deferred: separate
-  generic-instance objects; per-object self-hosting and definition-only
-  class vtables are covered.
+  default for the suite/corpus.  Type/value/procedure generic
+  instances, definition-only class vtables and per-object self-hosting
+  are covered; generic-class instances remain client-local.
 - **OO** is the Clarion-form `CLASS` dialect (single inheritance,
   virtual dispatch), not ISO 10514-3.
 

+ 4 - 3
docs/features.md

@@ -177,9 +177,10 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
 - ✅ Procedure-type variables / indirect calls (`t_proctype`); ✅
   composite function results (record/array/set/class, sret) and chained
   post-call method calls (`F().M()`); 🔄 per-file objects and `M2 -I`
-  scanning, content ABI/flags stamps and definition-only class objects
-  exist; separate generic-instance objects are deferred; import cycles
-  are rejected.
+  scanning, content ABI/flags stamps, definition-only class objects and
+  separate type/value/procedure generic-instance objects exist;
+  generic-class instances remain client-local; import cycles are
+  rejected.
 - 🔄 ISO exceptions: **M4** — `EXCEPT`/`RETRY`, handler stack,
   `SYSTEM.THROW`, `EXCEPTIONS`, `M2EXCEPTION`, signal mapping,
   `RETURN`/nested handlers, catchable index/range/CASE traps, and

+ 4 - 3
docs/plan-separate-compilation.md

@@ -5,7 +5,7 @@
 one-shot mode remains the default for the suite/corpus; this document
 keeps the design record and the deferred phases.
 
-**Baseline now:** suite 305/305, fixpoint OK 3,806,078 bytes, corpus
+**Baseline now:** suite 305/305, fixpoint OK 3,806,457 bytes, corpus
 519/612, 0 crashes.
 
 ---
@@ -154,8 +154,9 @@ cc Prog.o Lib.o runtime/syslib/shim.o -lm -o Prog
 3. **Init order + program/main split.**  DONE: library objects have no
    main, every object exports `<mod>_init`, and the program object calls
    imported inits in dependency order.
-4. **Generics and classes.**  Classes are done for the normal and
-   definition-only cases; generic instances are still client-local.
+4. **Generics and classes.**  Classes are done for normal and
+   definition-only cases.  Type/value/procedure generic instances get
+   their own objects; generic-class instances remain client-local.
 5. **M2make incremental builds.**  DONE: mtime stale checks, compiler
    binary/shim relink triggers, and a content ABI/flags stamp.
 6. **Self-host with separate compilation.**  DONE:

+ 5 - 3
docs/separate-compilation.md

@@ -70,9 +70,11 @@ The implementation is split across:
 - **Build freshness** uses mtimes plus a small content ABI/flags stamp
   (`gen_ssa/.m2abi`).  The stamp covers the ABI version and `--m2flag`
   settings; per-object binary metadata is not implemented.
-- **Generics** still need the generic implementation source at the
-  client; separate generic instances are not emitted as their own
-  objects yet.
+- **Type/value/procedure generic instances** are emitted as separate
+  objects by M2make: it detects `MODULE X = G(actuals)`, builds `X.o`
+  from the template plus instance declaration, and skips the abstract
+  generic object.  Generic-class instances still need the client
+  implementation path.
 - **Classes** have stable vtable/method symbols for normal imported
   class use.  M2make now also builds a definition-only object for a
   module with no `.mod`, so a class declared only in a `.def` gets its

+ 13 - 1
docs/summary_session-2026-10-24-separate-compilation.md

@@ -4,7 +4,7 @@ V3 now builds per-unit objects.  The one-shot mode remains the default
 for the suite/corpus.
 
 **Branch:** `master`.  **Feature state:** `v3-separate-compilation`.
-**Gates:** suite **305/305**, fixpoint **OK (3,806,078 bytes)**,
+**Gates:** suite **305/305**, fixpoint **OK (3,806,457 bytes)**,
 corpus **519/612 compile-OK, 0 crashes**.
 
 ---
@@ -90,6 +90,18 @@ Manual M2make cross-object class check: `CLib.def/.mod` + `CProg.mod`
   ABI stamp and definition-only object.
 - Suite is now 305/305.
 
+## Generic-instance objects (2026-10-24)
+
+- M2make detects `MODULE X = G(actuals)` in `.def`/`.mod` files and
+  records `G` as a dependency.
+- Abstract `GENERIC` modules are ordered but not compiled/linked.
+- An instance module is compiled to its own `gen_ssa/<X>.o` with the
+  generic template sources in the same session; the program links that
+  object.
+- Type, value and procedure generics work through separate objects.
+  Generic-class instances still use the client implementation path.
+- Added M2make `GenProg` and coverage for qualified procedure actuals.
+
 ## Commits and tags
 
 | commit | tag | work |

+ 2 - 1
m2make.md

@@ -74,7 +74,8 @@ event, and the link is redone when an object or the shim changes.
 ## Current limits
 
 - Incremental object checks use mtimes, not a content-based ABI marker.
-- Generic instances are still resolved at client compile time.
+- Type/value/procedure generic instances are built as separate objects;
+  generic-class instances still use the client implementation path.
 - The default one-shot mode remains the mode used by the suite, corpus and
   self-hosting fixpoint.
 - `M2 -c` resolves imported interfaces through `-I`; explicit interface

+ 120 - 50
tools/m2make/M2make.mod

@@ -42,6 +42,9 @@ VAR
   modScanned: ARRAY [0..255] OF BOOLEAN;
   modExternal: ARRAY [0..255] OF BOOLEAN;
   modIsMain: ARRAY [0..255] OF BOOLEAN;
+  modIsGeneric: ARRAY [0..255] OF BOOLEAN;
+  modIsInstance: ARRAY [0..255] OF BOOLEAN;
+  modInstGen: ARRAY [0..255] OF Name;
   nDep: ARRAY [0..255] OF CARDINAL;
   depTab: ARRAY [0..16383] OF INTEGER;
   state: ARRAY [0..255] OF CARDINAL;
@@ -60,6 +63,9 @@ VAR
   mainIdx: INTEGER;
   inComment: CARDINAL;
   gPendingFrom, gInImport, gSawHead, gWantName, gFromSeen: BOOLEAN;
+  gGenericNext, gHeadWasSaw, gAfterName, gWantGen: BOOLEAN;
+  gInInstActuals: BOOLEAN;
+  gLastActual: Name;
   tmpA, tmpB: Path;
   tmpN: Name;
   tmpP, tmpQ: Path;
@@ -315,6 +321,9 @@ BEGIN
   modScanned[nMods] := FALSE;
   modExternal[nMods] := FALSE;
   modIsMain[nMods] := FALSE;
+  modIsGeneric[nMods] := FALSE;
+  modIsInstance[nMods] := FALSE;
+  modInstGen[nMods][0] := CHR(0);
   nDep[nMods] := 0;
   INC(nMods);
   RETURN VAL(INTEGER, nMods - 1)
@@ -417,76 +426,79 @@ END ResolveModule;
 
 PROCEDURE HandleTok(tok: ARRAY OF CHAR; midx: INTEGER);
 BEGIN
+  IF Cmp(tok, "GENERIC") = 0 THEN
+    gGenericNext := TRUE;
+    gPendingFrom := FALSE; gInImport := FALSE;
+    gSawHead := FALSE; gWantName := FALSE; gFromSeen := FALSE;
+    RETURN
+  END;
   IF Cmp(tok, "FROM") = 0 THEN
     gPendingFrom := TRUE;
     gInImport := FALSE;
-    gSawHead := FALSE;
-    gWantName := FALSE;
-    gFromSeen := FALSE;
+    gSawHead := FALSE; gWantName := FALSE; gFromSeen := FALSE;
+    gAfterName := FALSE; gWantGen := FALSE;
     RETURN
   END;
   IF Cmp(tok, "IMPORT") = 0 THEN
     IF gPendingFrom OR gFromSeen THEN
-      (* FROM M IMPORT ... : following names are items, not modules *)
-      gPendingFrom := FALSE;
-      gFromSeen := FALSE
+      gPendingFrom := FALSE; gFromSeen := FALSE
     ELSE gInImport := TRUE
     END;
-    gSawHead := FALSE;
-    gWantName := FALSE;
+    gSawHead := FALSE; gWantName := FALSE;
     RETURN
   END;
-  IF Cmp(tok, "DEFINITION") = 0 THEN
-    gSawHead := TRUE;
-    gPendingFrom := FALSE;
-    gInImport := FALSE;
-    gWantName := FALSE;
-    gFromSeen := FALSE;
-    RETURN
-  END;
-  IF Cmp(tok, "IMPLEMENTATION") = 0 THEN
-    gSawHead := TRUE;
-    gPendingFrom := FALSE;
-    gInImport := FALSE;
-    gWantName := FALSE;
-    gFromSeen := FALSE;
+  IF (Cmp(tok, "DEFINITION") = 0) OR (Cmp(tok, "IMPLEMENTATION") = 0) THEN
+    gSawHead := TRUE; gHeadWasSaw := TRUE;
+    gPendingFrom := FALSE; gInImport := FALSE;
+    gWantName := FALSE; gFromSeen := FALSE;
     RETURN
   END;
   IF Cmp(tok, "MODULE") = 0 THEN
-    IF gSawHead THEN gWantName := FALSE
-    ELSE gWantName := TRUE
-    END;
+    gWantName := TRUE;
     gSawHead := FALSE;
-    gPendingFrom := FALSE;
-    gInImport := FALSE;
-    gFromSeen := FALSE;
+    gPendingFrom := FALSE; gInImport := FALSE; gFromSeen := FALSE;
+    gAfterName := FALSE; gWantGen := FALSE;
     RETURN
   END;
   IF gPendingFrom THEN
     AddDepName(midx, tok);
-    gPendingFrom := FALSE;
-    gFromSeen := TRUE;
-    gSawHead := FALSE;
+    gPendingFrom := FALSE; gFromSeen := TRUE; gSawHead := FALSE;
     RETURN
   END;
   IF gInImport THEN
     AddDepName(midx, tok);
-    gSawHead := FALSE;
-    gFromSeen := FALSE;
+    gSawHead := FALSE; gFromSeen := FALSE;
     RETURN
   END;
   IF gWantName THEN
-    gWantName := FALSE;
-    IF wantProg AND (midx = mainIdx) THEN
-      Copy(tok, progName);
-      wantProg := FALSE
+    gWantName := FALSE; gAfterName := TRUE;
+    IF gGenericNext THEN
+      modIsGeneric[midx] := TRUE; gGenericNext := FALSE
     END;
-    gSawHead := FALSE;
-    gFromSeen := FALSE;
+    IF (NOT gHeadWasSaw) AND wantProg AND (midx = mainIdx) THEN
+      Copy(tok, progName); wantProg := FALSE
+    END;
+    gHeadWasSaw := FALSE; gSawHead := FALSE; gFromSeen := FALSE;
     RETURN
   END;
-  gSawHead := FALSE;
-  gFromSeen := FALSE
+  IF gAfterName AND (Cmp(tok, "=") = 0) THEN
+    gAfterName := FALSE; gWantGen := TRUE;
+    gInInstActuals := FALSE; gLastActual[0] := CHR(0);
+    RETURN
+  END;
+  IF gWantGen THEN
+    AddDepName(midx, tok);
+    Copy(tok, modInstGen[midx]);
+    modIsInstance[midx] := TRUE;
+    gWantGen := FALSE; gAfterName := FALSE;
+    gInInstActuals := TRUE; gLastActual[0] := CHR(0);
+    RETURN
+  END;
+  IF gInInstActuals THEN
+    Copy(tok, gLastActual)
+  END;
+  gSawHead := FALSE; gFromSeen := FALSE;
+  gAfterName := FALSE
 END HandleTok;
 
 PROCEDURE ScanLine(line: ARRAY OF CHAR; midx: INTEGER);
@@ -520,12 +532,29 @@ BEGIN
         i := n
       ELSIF (ch = "'") OR (ch = '"') THEN
         inStr := TRUE; qc := ch; INC(i)
+      ELSIF ch = "." THEN
+        IF gInInstActuals AND (gLastActual[0] # CHR(0)) THEN
+          AddDepName(midx, gLastActual);
+          gLastActual[0] := CHR(0)
+        END;
+        INC(i)
+      ELSIF ch = "=" THEN
+        IF gAfterName THEN
+          gAfterName := FALSE; gWantGen := TRUE
+        END;
+        INC(i)
       ELSIF ch = ";" THEN
         gPendingFrom := FALSE;
         gInImport := FALSE;
         gSawHead := FALSE;
         gWantName := FALSE;
         gFromSeen := FALSE;
+        gGenericNext := FALSE; gHeadWasSaw := FALSE;
+        gAfterName := FALSE; gWantGen := FALSE;
+        gInInstActuals := FALSE; gLastActual[0] := CHR(0);
+        gAfterName := FALSE;
+        gWantGen := FALSE;
+        gInInstActuals := FALSE; gLastActual[0] := CHR(0);
         INC(i)
       ELSIF IsLetter(ch) THEN
         k := i;
@@ -863,6 +892,10 @@ BEGIN
   WHILE k < nOrder DO
     GetDef(order[k], tmpP);
     IF SrcNewer(tmpP, ot) THEN RETURN TRUE END;
+    IF modIsGeneric[order[k]] THEN
+      GetSrc(order[k], tmpP);
+      IF SrcNewer(tmpP, ot) THEN RETURN TRUE END
+    END;
     INC(k)
   END;
   RETURN FALSE
@@ -910,17 +943,25 @@ PROCEDURE BuildM2Separate(out: ARRAY OF CHAR);
    object with main; every module object exports <mod>_init. *)
 VAR cmd: Cmd; k, m: CARDINAL; rc: INTEGER;
     obj, src, ssafile, sfile, flags: Path;
-    outT: LONGINT; compiledAny, linkNeeded, abiOK: BOOLEAN;
+    outT: LONGINT; compiledAny, linkNeeded, abiOK, hasGeneric: BOOLEAN;
+    gi, k2: INTEGER;
 BEGIN
   Zero(cmd);
   abiOK := AbiStampOK();
   compiledAny := FALSE;
+  hasGeneric := FALSE;
+  k2 := 0;
+  WHILE k2 < nOrder DO
+    IF modIsGeneric[order[k2]] THEN hasGeneric := TRUE END;
+    INC(k2)
+  END;
   k := 0;
   WHILE k < nOrder DO
     m := order[k];
     GetSrc(m, src);
     GetDef(m, tmpQ);
-    IF (Len(src) > 0) OR ((NOT modIsMain[m]) AND (Len(tmpQ) > 0)) THEN
+    IF (NOT modIsGeneric[m]) AND
+       ((Len(src) > 0) OR ((NOT modIsMain[m]) AND (Len(tmpQ) > 0))) THEN
       IF Len(src) = 0 THEN
         (* A definition-only module still contributes exported data and
            class vtables to the link. *)
@@ -939,13 +980,41 @@ BEGIN
           AppendSeg(tmpCmd, " ");
           AppendSeg(tmpCmd, m2Flags)
         END;
-        (* M2 resolves the target .def and all imported .defs from -I. *)
-        IFlags(flags);
-        IF Len(flags) > 0 THEN
-          AppendSeg(tmpCmd, " ");
-          AppendSeg(tmpCmd, flags)
+        IF hasGeneric THEN
+          (* A generic instance needs its template in the same session.
+             Keep the explicit closure small: all interfaces + generic
+             implementations + the target source. *)
+          k2 := 0;
+          WHILE k2 < nOrder DO
+            GetDef(order[k2], tmpP);
+            IF Len(tmpP) > 0 THEN AppendTok(tmpCmd, tmpP) END;
+            IF modIsGeneric[order[k2]] THEN
+              GetSrc(order[k2], tmpP);
+              IF Len(tmpP) > 0 THEN AppendTok(tmpCmd, tmpP) END
+            END;
+            INC(k2)
+          END;
+          AppendTok(tmpCmd, src)
+        ELSE
+          (* M2 resolves the target .def and all imported .defs from -I. *)
+          IFlags(flags);
+          IF Len(flags) > 0 THEN
+            AppendSeg(tmpCmd, " ");
+            AppendSeg(tmpCmd, flags)
+          END;
+          IF modIsInstance[m] THEN
+            gi := FindMod(modInstGen[m]);
+            IF gi >= 0 THEN
+              GetDef(VAL(CARDINAL, gi), tmpP);
+              IF Len(tmpP) > 0 THEN AppendTok(tmpCmd, tmpP) END;
+              GetSrc(VAL(CARDINAL, gi), tmpP);
+              IF Len(tmpP) > 0 THEN AppendTok(tmpCmd, tmpP) END
+            END;
+            GetDef(m, tmpP);
+            IF Len(tmpP) > 0 THEN AppendTok(tmpCmd, tmpP) END
+          END;
+          AppendTok(tmpCmd, src)
         END;
-        AppendTok(tmpCmd, src);
         AppendSeg(tmpCmd, " 2>&1 | tee m2make.log | grep -q 'Parsed correctly'");
         AppendSeg(tmpCmd, " && qbe -o ");
         AppendTok(tmpCmd, sfile);
@@ -1005,7 +1074,8 @@ BEGIN
     m := order[k];
     GetSrc(m, src);
     GetDef(m, tmpQ);
-    IF (Len(src) > 0) OR ((NOT modIsMain[m]) AND (Len(tmpQ) > 0)) THEN
+    IF (NOT modIsGeneric[m]) AND
+       ((Len(src) > 0) OR ((NOT modIsMain[m]) AND (Len(tmpQ) > 0))) THEN
       IF modIsMain[m] THEN Copy(progName, tmpN) ELSE GetName(m, tmpN) END;
       Zero(obj); Cat("gen_ssa/", tmpN, tmpA); Cat(tmpA, ".o", obj);
       AppendTok(tmpCmd, obj)

+ 6 - 0
tools/m2make/tests/GenProg.mod

@@ -0,0 +1,6 @@
+MODULE GenericProg;
+FROM IntId IMPORT Id;
+VAR ExitCode : INTEGER;
+BEGIN
+  ExitCode := Id(42)
+END GenericProg.

+ 3 - 0
tools/m2make/tests/Ident.def

@@ -0,0 +1,3 @@
+GENERIC DEFINITION MODULE Ident (T : TYPE);
+PROCEDURE Id(x : T) : T;
+END Ident.

+ 4 - 0
tools/m2make/tests/Ident.mod

@@ -0,0 +1,4 @@
+GENERIC IMPLEMENTATION MODULE Ident (T : TYPE);
+PROCEDURE Id(x : T) : T;
+BEGIN RETURN x END Id;
+END Ident.

+ 2 - 0
tools/m2make/tests/IntId.def

@@ -0,0 +1,2 @@
+DEFINITION MODULE IntId = Ident(INTEGER);
+END IntId.

+ 2 - 0
tools/m2make/tests/IntId.mod

@@ -0,0 +1,2 @@
+IMPLEMENTATION MODULE IntId = Ident(INTEGER);
+END IntId.

+ 12 - 1
tools/m2make/tests/run_tests.sh

@@ -142,6 +142,17 @@ else
   no "separate-mode definition-only class object (build)"
 fi
 
+# --- generic instance as its own object ---
+rm -rf gen_ssa GenProg
+if $V3M --separate -c m2 --m2bin ../../compiler/M2 --shim ../../runtime/syslib/shim.c \
+     -I . -o GenProg tests/GenProg.mod > /tmp/m2make_t.log 2>&1; then
+  ./GenProg > /dev/null 2>&1; rc=$?
+  if [ "$rc" = "42" ]; then ok "separate-mode generic instance object"
+  else no "separate-mode generic instance object (rc=$rc)"; fi
+else
+  no "separate-mode generic instance object (build)"
+fi
+
 # --- content ABI stamp ---
 rm -f gen_ssa/.m2abi
 out=$($V3M --separate -c m2 --m2bin ../../compiler/M2 --shim ../../runtime/syslib/shim.c \
@@ -161,7 +172,7 @@ else
   no "gm2-built M2make drives m2 build"
 fi
 
-rm -f Main MainX Main2 SepMain CDefProg *.o
+rm -f Main MainX Main2 SepMain CDefProg GenProg *.o
 rm -rf gen_ssa
 rm -f m2make.log