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

feat: add ABI stamp and definition-only objects

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

+ 2 - 2
README.md

@@ -87,13 +87,13 @@ docs/              STATUS.md, plan.md, plan-*.md, analysis_*.md, summary_*.md
 
 
 ## Status (2026-10-24)
 ## Status (2026-10-24)
 
 
-- Suite **304/304**; self-hosting **fixpoint OK** (byte-identical
+- Suite **305/305**; self-hosting **fixpoint OK** (byte-identical
   stage2/stage3, 3,806,078 bytes); corpus **519/612 compile-OK,
   stage2/stage3, 3,806,078 bytes); corpus **519/612 compile-OK,
   0 crashes** (`tools/v3-corpus`; the corpus is gm2's testsuite, so it
   0 crashes** (`tools/v3-corpus`; the corpus is gm2's testsuite, so it
   mixes conforming Modula-2 with gm2 leniencies — see `docs/gaps/`).
   mixes conforming Modula-2 with gm2 leniencies — see `docs/gaps/`).
   The remaining `except*`/`onebyte*`-style corpus files still hit V3's
   The remaining `except*`/`onebyte*`-style corpus files still hit V3's
   declaration-order policy.
   declaration-order policy.
-- Active branch **`master`**, feature state **`v3-separate-fixpoint`**.
+- Active branch **`master`**, feature state **`v3-separate-abi-vtables`**.
   Recent milestones: `PACKEDSET`, ISO exceptions (`EXCEPT`/`RETRY`,
   Recent milestones: `PACKEDSET`, ISO exceptions (`EXCEPT`/`RETRY`,
   `EXCEPTIONS`, `M2EXCEPTION`, catchable language traps including
   `EXCEPTIONS`, `M2EXCEPTION`, catchable language traps including
   `functionException`, module `FINALLY`), self-referential pointer
   `functionException`, module `FINALLY`), self-referential pointer

+ 4 - 3
TUTORIAL.md

@@ -10,7 +10,7 @@ From the repository root:
 ```sh
 ```sh
 cd compiler
 cd compiler
 ./build.sh          # M2.atg -> scanner/parser/driver -> ./M2
 ./build.sh          # M2.atg -> scanner/parser/driver -> ./M2
-./run_tests.sh      # 304/304
+./run_tests.sh      # 305/305
 ```
 ```
 
 
 `M2` reads Modula-2 sources and writes QBE IR to `gen_ssa/`.
 `M2` reads Modula-2 sources and writes QBE IR to `gen_ssa/`.
@@ -149,7 +149,7 @@ TMP="$PWD/.scratch/fixpoint-separate" ./bootstrap/fixpoint-separate.sh
 tools/v3-corpus/corpus.sh
 tools/v3-corpus/corpus.sh
 ```
 ```
 
 
-Current gates: suite **304/304**, fixpoint **OK (3,806,078 bytes)**,
+Current gates: suite **305/305**, fixpoint **OK (3,806,078 bytes)**,
 corpus **519/612 compile-OK, 0 crashes**.
 corpus **519/612 compile-OK, 0 crashes**.
 
 
 ## 7. Current limits
 ## 7. Current limits
@@ -157,7 +157,8 @@ corpus **519/612 compile-OK, 0 crashes**.
 - `M2 -I` transitive interface scanning is implemented.
 - `M2 -I` transitive interface scanning is implemented.
 - `M2make --separate` rebuilds only stale objects, relinks when an object,
 - `M2make --separate` rebuilds only stale objects, relinks when an object,
   the shim, or the output changes, and treats a newer compiler binary as an
   the shim, or the output changes, and treats a newer compiler binary as an
-  ABI event.  There is no content-based ABI version marker yet.
+  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.
 - Generic instance objects are not emitted separately yet.
 - Per-object self-hosting is a green gate via
 - Per-object self-hosting is a green gate via
   `bootstrap/fixpoint-separate.sh`; compiler units use
   `bootstrap/fixpoint-separate.sh`; compiler units use

+ 1 - 0
compiler/run_tests.sh

@@ -754,6 +754,7 @@ expect_separate_auto() {
 }
 }
 
 
 expect_separate_auto SepResProg 42 SepRes sepres.mod sepres_prog.mod
 expect_separate_auto SepResProg 42 SepRes sepres.mod sepres_prog.mod
+expect_separate_auto CDefProg 8 CDef cdef.def cdef_prog.mod
 
 
 # --- M2 process exit codes: 0 = ok, 1 = compile errors, 2 = usage/IO ---
 # --- M2 process exit codes: 0 = ok, 1 = compile errors, 2 = usage/IO ---
 check_exit() {
 check_exit() {

+ 8 - 0
compiler/tests/cdef.def

@@ -0,0 +1,8 @@
+DEFINITION MODULE CDef;
+(* Definition-only class: the separate build must still emit its vtable. *)
+TYPE
+  CLASS Shape;
+    x : INTEGER;
+    VIRTUAL PROCEDURE Area () : INTEGER;
+  END Shape;
+END CDef.

+ 8 - 0
compiler/tests/cdef_prog.mod

@@ -0,0 +1,8 @@
+MODULE CDefProg;
+IMPORT CDef;
+VAR ExitCode : INTEGER;
+VAR s : CDef.Shape;
+BEGIN
+  s.x := 8;
+  ExitCode := s.x
+END CDefProg.

+ 4 - 4
docs/STATUS.md

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

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

@@ -2,7 +2,7 @@
 
 
 **Date:** 2026-10-24 · **V3 state:** open arrays, dialect library
 **Date:** 2026-10-24 · **V3 state:** open arrays, dialect library
 folders and separate compilation with `M2 -I`/incremental objects
 folders and separate compilation with `M2 -I`/incremental objects
-(suite 304/304, self-hosting fixpoint OK 3,806,078 bytes, corpus
+(suite 305/305, self-hosting fixpoint OK 3,806,078 bytes, corpus
 519/612, 0 crashes).
 519/612, 0 crashes).
 
 
 **Scope and sources.**
 **Scope and sources.**
@@ -118,7 +118,7 @@ change individual cells.
 | Local modules | ✅ | ✅ | ❌ removed | ✅ | ✅ |
 | Local modules | ✅ | ✅ | ❌ removed | ✅ | ✅ |
 | Separate compilation | ✅ | ✅ (object files/archives) | ✅ compilation units | ✅ | ✅ `M2 -c -I` per-unit objects + `M2make --separate`; one-shot remains default |
 | Separate compilation | ✅ | ✅ (object files/archives) | ✅ compilation units | ✅ | ✅ `M2 -c -I` per-unit objects + `M2make --separate`; one-shot remains default |
 | Stable cross-object symbols / init order | n/a | ✅ | ✅ | ✅ | ✅ `<Module>__proc`, module-qualified pools/vtables, dependency-ordered `<Module>_init` |
 | Stable cross-object symbols / init order | n/a | ✅ | ✅ | ✅ | ✅ `<Module>__proc`, module-qualified pools/vtables, dependency-ordered `<Module>_init` |
-| Incremental object rebuild / ABI marker | n/a | ✅ | ✅ | ✅ | 🔄 stale mtime rebuilds, compiler/shim ABI/relink triggers; no content-based ABI marker |
+| Incremental object rebuild / ABI marker | n/a | ✅ | ✅ | ✅ | ✅ stale mtime rebuilds, content ABI/flags stamp, compiler/shim relink triggers |
 | Generics | ❌ | ✅ ISO 10514-2 | ❌ external templates (`GENLIB`/`BLUEPRINT`) | ❌ | ✅ ISO 10514-2 refinement |
 | Generics | ❌ | ✅ ISO 10514-2 | ❌ external templates (`GENLIB`/`BLUEPRINT`) | ❌ | ✅ ISO 10514-2 refinement |
 | Object orientation | ❌ | ❌ | ❌ (extensible records only) | ✅ Clarion-form `CLASS`, `VIRTUAL`, inheritance | 🔄 Clarion-form `CLASS` (single inheritance, virtual dispatch), not ISO 10514-3 |
 | Object orientation | ❌ | ❌ | ❌ (extensible records only) | ✅ Clarion-form `CLASS`, `VIRTUAL`, inheritance | 🔄 Clarion-form `CLASS` (single inheritance, virtual dispatch), not ISO 10514-3 |
 | PIM coroutines (`PROCESS`, `NEWPROCESS`, `TRANSFER`, `IOTRANSFER`) | ✅ | ✅ | 🔄 `COROUTINE` type/`YIELD` (R10 phase II) | ✅ PIM-style | ✅ (`IOTRANSFER` degrades to a transfer) |
 | PIM coroutines (`PROCESS`, `NEWPROCESS`, `TRANSFER`, `IOTRANSFER`) | ✅ | ✅ | 🔄 `COROUTINE` type/`YIELD` (R10 phase II) | ✅ PIM-style | ✅ (`IOTRANSFER` degrades to a transfer) |
@@ -204,9 +204,9 @@ change individual cells.
   `<Module>__<Class>__vt`.  `M2 -I` resolves interfaces transitively;
   `<Module>__<Class>__vt`.  `M2 -I` resolves interfaces transitively;
   `M2make --separate` rebuilds stale objects and relinks on source/
   `M2make --separate` rebuilds stale objects and relinks on source/
   interface/compiler/shim changes.  The one-shot mode remains the
   interface/compiler/shim changes.  The one-shot mode remains the
-  default for the suite/corpus.  Still deferred: content-based ABI
-  markers, separate generic-instance objects and a green per-object
-  self-host fixpoint.
+  default for the suite/corpus.  Still deferred: separate
+  generic-instance objects; per-object self-hosting and definition-only
+  class vtables are covered.
 - **OO** is the Clarion-form `CLASS` dialect (single inheritance,
 - **OO** is the Clarion-form `CLASS` dialect (single inheritance,
   virtual dispatch), not ISO 10514-3.
   virtual dispatch), not ISO 10514-3.
 
 

+ 4 - 3
docs/features.md

@@ -1,4 +1,4 @@
-# m2compiler-V3 — feature status (2026-10-24; suite 304/304 green)
+# m2compiler-V3 — feature status (2026-10-24; suite 305/305 green)
 
 
 Pipeline: Coco/R `M2.atg` (1748 lines, 73 productions) → `gm2`-built
 Pipeline: Coco/R `M2.atg` (1748 lines, 73 productions) → `gm2`-built
 `M2` → QBE `.ssa` → `qbe` → `cc` → run. `SymTab.mod` 1623 lines,
 `M2` → QBE `.ssa` → `qbe` → `cc` → run. `SymTab.mod` 1623 lines,
@@ -177,8 +177,9 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
 - ✅ Procedure-type variables / indirect calls (`t_proctype`); ✅
 - ✅ Procedure-type variables / indirect calls (`t_proctype`); ✅
   composite function results (record/array/set/class, sret) and chained
   composite function results (record/array/set/class, sret) and chained
   post-call method calls (`F().M()`); 🔄 per-file objects and `M2 -I`
   post-call method calls (`F().M()`); 🔄 per-file objects and `M2 -I`
-  scanning exist; content-based ABI markers and generic-instance objects
-  are deferred; import cycles are rejected.
+  scanning, content ABI/flags stamps and definition-only class objects
+  exist; separate generic-instance objects are deferred; import cycles
+  are rejected.
 - 🔄 ISO exceptions: **M4** — `EXCEPT`/`RETRY`, handler stack,
 - 🔄 ISO exceptions: **M4** — `EXCEPT`/`RETRY`, handler stack,
   `SYSTEM.THROW`, `EXCEPTIONS`, `M2EXCEPTION`, signal mapping,
   `SYSTEM.THROW`, `EXCEPTIONS`, `M2EXCEPTION`, signal mapping,
   `RETURN`/nested handlers, catchable index/range/CASE traps, and
   `RETURN`/nested handlers, catchable index/range/CASE traps, and

+ 5 - 7
docs/plan-separate-compilation.md

@@ -5,7 +5,7 @@
 one-shot mode remains the default for the suite/corpus; this document
 one-shot mode remains the default for the suite/corpus; this document
 keeps the design record and the deferred phases.
 keeps the design record and the deferred phases.
 
 
-**Baseline now:** suite 304/304, fixpoint OK 3,806,078 bytes, corpus
+**Baseline now:** suite 305/305, fixpoint OK 3,806,078 bytes, corpus
 519/612, 0 crashes.
 519/612, 0 crashes.
 
 
 ---
 ---
@@ -154,12 +154,10 @@ cc Prog.o Lib.o runtime/syslib/shim.o -lm -o Prog
 3. **Init order + program/main split.**  DONE: library objects have no
 3. **Init order + program/main split.**  DONE: library objects have no
    main, every object exports `<mod>_init`, and the program object calls
    main, every object exports `<mod>_init`, and the program object calls
    imported inits in dependency order.
    imported inits in dependency order.
-4. **Generics and classes.**  PARTIAL: class vtable/method symbols are
-   stable across objects; generic instances are still client-local.
-5. **M2make incremental builds.**  DONE at mtime level: stale objects
-   are rebuilt, the compiler binary is an ABI trigger, and relinking
-   follows object/shim changes.  Content-based ABI markers are still
-   deferred.
+4. **Generics and classes.**  Classes are done for the normal and
+   definition-only cases; generic instances are still 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:
 6. **Self-host with separate compilation.**  DONE:
    `bootstrap/fixpoint-separate.sh` builds stage2/stage3 as objects and
    `bootstrap/fixpoint-separate.sh` builds stage2/stage3 as objects and
    compares all per-object `.ssa`; the compiler uses
    compares all per-object `.ssa`; the compiler uses

+ 6 - 5
docs/separate-compilation.md

@@ -67,15 +67,16 @@ The implementation is split across:
 
 
 ## Limits / next phases
 ## Limits / next phases
 
 
-- **Incremental checks are mtime-based.**  Sources/interfaces, the
-  compiler binary and the shim trigger rebuilds/relinks; there is no
-  content-based ABI/version marker or flag fingerprint.
+- **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
 - **Generics** still need the generic implementation source at the
   client; separate generic instances are not emitted as their own
   client; separate generic instances are not emitted as their own
   objects yet.
   objects yet.
 - **Classes** have stable vtable/method symbols for normal imported
 - **Classes** have stable vtable/method symbols for normal imported
-  class use; a class that exists only in a `.def` (no implementation
-  object) must still have its vtable materialised by a user object.
+  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
+  (zero-method) vtable object.
 - **Per-object self-hosting is green:** `bootstrap/fixpoint-separate.sh`
 - **Per-object self-hosting is green:** `bootstrap/fixpoint-separate.sh`
   builds the compiler as objects with stage1, links stage2, rebuilds with
   builds the compiler as objects with stage1, links stage2, rebuilds with
   stage2, links stage3, and compares all per-object `.ssa`.  Compiler
   stage2, links stage3, and compares all per-object `.ssa`.  Compiler

+ 12 - 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.
 for the suite/corpus.
 
 
 **Branch:** `master`.  **Feature state:** `v3-separate-compilation`.
 **Branch:** `master`.  **Feature state:** `v3-separate-compilation`.
-**Gates:** suite **304/304**, fixpoint **OK (3,806,078 bytes)**,
+**Gates:** suite **305/305**, fixpoint **OK (3,806,078 bytes)**,
 corpus **519/612 compile-OK, 0 crashes**.
 corpus **519/612 compile-OK, 0 crashes**.
 
 
 ---
 ---
@@ -79,6 +79,17 @@ Manual M2make cross-object class check: `CLib.def/.mod` + `CProg.mod`
   builds stage2/stage3 as objects and compares all per-object `.ssa`
   builds stage2/stage3 as objects and compares all per-object `.ssa`
   (11 objects).
   (11 objects).
 
 
+## Follow-up — ABI stamp and definition-only objects (2026-10-24)
+
+- `M2make --separate` now records `gen_ssa/.m2abi` containing the ABI
+  version and `--m2flag` settings; a missing/changed stamp rebuilds all
+  objects.
+- Modules with a `.def` but no `.mod` are compiled to
+  `gen_ssa/<Module>.o`, so definition-only data and class vtables link.
+- Added `CDefProg` (class-only `.def` vtable) and M2make checks for the
+  ABI stamp and definition-only object.
+- Suite is now 305/305.
+
 ## Commits and tags
 ## Commits and tags
 
 
 | commit | tag | work |
 | commit | tag | work |

+ 58 - 6
tools/m2make/M2make.mod

@@ -15,6 +15,8 @@ MODULE M2make;
 IMPORT FileIO, M2makeOS;
 IMPORT FileIO, M2makeOS;
 
 
 CONST
 CONST
+  AbiVersion = "v3-separate-abi-1";
+  AbiStampPath = "gen_ssa/.m2abi";
   MaxMods = 256;
   MaxMods = 256;
   MaxDeps = 64;
   MaxDeps = 64;
   MaxDirs = 16;
   MaxDirs = 16;
@@ -866,24 +868,67 @@ BEGIN
   RETURN FALSE
   RETURN FALSE
 END SepObjStale;
 END SepObjStale;
 
 
+PROCEDURE AbiStampOK (): BOOLEAN;
+(* Content check for the object ABI/flags stamp.  This is deliberately
+   a small build-system marker, not a per-object binary format. *)
+VAR h: M2makeOS.RdFile; line: Line; want: Path; ok: BOOLEAN;
+BEGIN
+  h := M2makeOS.OpenRead(AbiStampPath);
+  IF h = NIL THEN RETURN FALSE END;
+  ok := M2makeOS.ReadLine(h, line, HIGH(line) + 1);
+  M2makeOS.CloseRead(h);
+  IF NOT ok THEN RETURN FALSE END;
+  Copy(AbiVersion, want);
+  IF Len(m2Flags) > 0 THEN
+    Cat(want, " ", tmpA);
+    Cat(tmpA, m2Flags, want)
+  END;
+  RETURN Cmp(line, want) = 0
+END AbiStampOK;
+
+PROCEDURE WriteAbiStamp;
+VAR cmd: Cmd;
+BEGIN
+  Zero(cmd);
+  AppendSeg(cmd, "mkdir -p gen_ssa && printf '%s\n' '");
+  AppendSeg(cmd, AbiVersion);
+  IF Len(m2Flags) > 0 THEN
+    AppendSeg(cmd, " ");
+    AppendSeg(cmd, m2Flags)
+  END;
+  AppendSeg(cmd, "' > ");
+  AppendSeg(cmd, AbiStampPath);
+  IF M2makeOS.ExecCmd(cmd) # 0 THEN
+    Out("M2make: could not write ABI stamp"); OutLn;
+    M2makeOS.ExitNow(1)
+  END
+END WriteAbiStamp;
+
 PROCEDURE BuildM2Separate(out: ARRAY OF CHAR);
 PROCEDURE BuildM2Separate(out: ARRAY OF CHAR);
 (* Compile stale modules to their own QBE objects, then link.  The
 (* Compile stale modules to their own QBE objects, then link.  The
    program module is last in the topological order and is the only
    program module is last in the topological order and is the only
    object with main; every module object exports <mod>_init. *)
    object with main; every module object exports <mod>_init. *)
 VAR cmd: Cmd; k, m: CARDINAL; rc: INTEGER;
 VAR cmd: Cmd; k, m: CARDINAL; rc: INTEGER;
     obj, src, ssafile, sfile, flags: Path;
     obj, src, ssafile, sfile, flags: Path;
-    outT: LONGINT; compiledAny, linkNeeded: BOOLEAN;
+    outT: LONGINT; compiledAny, linkNeeded, abiOK: BOOLEAN;
 BEGIN
 BEGIN
   Zero(cmd);
   Zero(cmd);
+  abiOK := AbiStampOK();
   compiledAny := FALSE;
   compiledAny := FALSE;
   k := 0;
   k := 0;
   WHILE k < nOrder DO
   WHILE k < nOrder DO
     m := order[k];
     m := order[k];
     GetSrc(m, src);
     GetSrc(m, src);
-    IF Len(src) > 0 THEN
+    GetDef(m, tmpQ);
+    IF (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. *)
+        Copy(tmpQ, src)
+      END;
       IF modIsMain[m] THEN Copy(progName, tmpN) ELSE GetName(m, tmpN) END;
       IF modIsMain[m] THEN Copy(progName, tmpN) ELSE GetName(m, tmpN) END;
       Zero(obj); Cat("gen_ssa/", tmpN, tmpA); Cat(tmpA, ".o", obj);
       Zero(obj); Cat("gen_ssa/", tmpN, tmpA); Cat(tmpA, ".o", obj);
-      IF SepObjStale(m, obj) THEN
+      IF (NOT abiOK) OR SepObjStale(m, obj) THEN
         CpyPath(ssafile, "gen_ssa/"); Cat(ssafile, tmpN, tmpA); Cat(tmpA, ".ssa", ssafile);
         CpyPath(ssafile, "gen_ssa/"); Cat(ssafile, tmpN, tmpA); Cat(tmpA, ".ssa", ssafile);
         CpyPath(sfile, "gen_ssa/"); Cat(sfile, tmpN, tmpA); Cat(tmpA, ".s", sfile);
         CpyPath(sfile, "gen_ssa/"); Cat(sfile, tmpN, tmpA); Cat(tmpA, ".s", sfile);
         Zero(tmpCmd);
         Zero(tmpCmd);
@@ -959,7 +1004,8 @@ BEGIN
   WHILE k < nOrder DO
   WHILE k < nOrder DO
     m := order[k];
     m := order[k];
     GetSrc(m, src);
     GetSrc(m, src);
-    IF Len(src) > 0 THEN
+    GetDef(m, tmpQ);
+    IF (Len(src) > 0) OR ((NOT modIsMain[m]) AND (Len(tmpQ) > 0)) THEN
       IF modIsMain[m] THEN Copy(progName, tmpN) ELSE GetName(m, tmpN) END;
       IF modIsMain[m] THEN Copy(progName, tmpN) ELSE GetName(m, tmpN) END;
       Zero(obj); Cat("gen_ssa/", tmpN, tmpA); Cat(tmpA, ".o", obj);
       Zero(obj); Cat("gen_ssa/", tmpN, tmpA); Cat(tmpA, ".o", obj);
       AppendTok(tmpCmd, obj)
       AppendTok(tmpCmd, obj)
@@ -980,7 +1026,8 @@ BEGIN
     Out("M2make: separate build failed");
     Out("M2make: separate build failed");
     OutLn;
     OutLn;
     M2makeOS.ExitNow(1)
     M2makeOS.ExitNow(1)
-  END
+  END;
+  WriteAbiStamp
 END BuildM2Separate;
 END BuildM2Separate;
 
 
 PROCEDURE CpyPath(VAR dst: Path; src: ARRAY OF CHAR);
 PROCEDURE CpyPath(VAR dst: Path; src: ARRAY OF CHAR);
@@ -1288,7 +1335,12 @@ BEGIN
   IF NOT outGiven THEN Copy(progName, outName) END;
   IF NOT outGiven THEN Copy(progName, outName) END;
   TopoSort;
   TopoSort;
   IF verbose OR dryRun THEN PrintOrder END;
   IF verbose OR dryRun THEN PrintOrder END;
-  rebuild := NeedBuild(outName, missing);
+  IF separate THEN
+    (* BuildM2Separate owns per-object freshness and the ABI stamp. *)
+    rebuild := TRUE
+  ELSE
+    rebuild := NeedBuild(outName, missing)
+  END;
   IF NOT rebuild THEN
   IF NOT rebuild THEN
     Out("M2make: ");
     Out("M2make: ");
     Out(outName);
     Out(outName);

+ 2 - 2
tools/m2make/README.md

@@ -33,8 +33,8 @@ M2make [-n] [-v] [-c m2|gm2] [-I dir] [-o out]
   `M2 -c -I ...`, then link the objects with `cc` and the runtime shim.
   `M2 -c -I ...`, then link the objects with `cc` and the runtime shim.
   The program object is the only one with `main`; library objects export
   The program object is the only one with `main`; library objects export
   `<Module>_init`.  Source/interface mtimes, a newer compiler binary and
   `<Module>_init`.  Source/interface mtimes, a newer compiler binary and
-  the shim drive rebuild/relink; there is no content-based ABI marker
-  yet.
+  the shim drive rebuild/relink, and `gen_ssa/.m2abi` records the
+  compiler ABI/flags content.
 
 
 `m2` mode (default, one-shot) runs one session — `<m2bin> <ordered .def/.mod files>`
 `m2` mode (default, one-shot) runs one session — `<m2bin> <ordered .def/.mod files>`
 then `qbe` then `cc <image>.s <shim> [<extra>] -o <out> -lm` — and
 then `qbe` then `cc <image>.s <shim> [<extra>] -o <out> -lm` — and

+ 8 - 0
tools/m2make/tests/CDef.def

@@ -0,0 +1,8 @@
+DEFINITION MODULE CDef;
+(* Definition-only class for separate-object M2make tests. *)
+TYPE
+  CLASS Shape;
+    x : INTEGER;
+    VIRTUAL PROCEDURE Area () : INTEGER;
+  END Shape;
+END CDef.

+ 8 - 0
tools/m2make/tests/CProg.mod

@@ -0,0 +1,8 @@
+MODULE CProg;
+IMPORT CDef;
+VAR ExitCode : INTEGER;
+VAR s : CDef.Shape;
+BEGIN
+  s.x := 8;
+  ExitCode := s.x
+END CProg.

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

@@ -131,6 +131,26 @@ case "$out" in
 esac
 esac
 touch tests/Calc.def tests/Calc.mod tests/Main.mod
 touch tests/Calc.def tests/Calc.mod tests/Main.mod
 
 
+# --- definition-only class object ---
+rm -rf gen_ssa CDefProg
+if $V3M --separate -c m2 --m2bin ../../compiler/M2 --shim ../../runtime/syslib/shim.c \
+     -I . -o CDefProg tests/CProg.mod > /tmp/m2make_t.log 2>&1; then
+  ./CDefProg > /dev/null 2>&1; rc=$?
+  if [ "$rc" = "8" ]; then ok "separate-mode definition-only class object"
+  else no "separate-mode definition-only class object (rc=$rc)"; fi
+else
+  no "separate-mode definition-only class object (build)"
+fi
+
+# --- content ABI stamp ---
+rm -f gen_ssa/.m2abi
+out=$($V3M --separate -c m2 --m2bin ../../compiler/M2 --shim ../../runtime/syslib/shim.c \
+  -I . -o CDefProg tests/CProg.mod 2>&1)
+case "$out" in
+  *"tests/CProg.mod"*) ok "separate-mode ABI stamp forces rebuild" ;;
+  *) no "separate-mode ABI stamp: $out" ;;
+esac
+
 # --- cross-check: gm2-built M2make drives an m2 build too ---
 # --- cross-check: gm2-built M2make drives an m2 build too ---
 rm -rf gen_ssa MainX m2make.log
 rm -rf gen_ssa MainX m2make.log
 if $GM2 -c m2 --m2bin ../../compiler/M2 --shim ../../runtime/syslib/shim.c \
 if $GM2 -c m2 --m2bin ../../compiler/M2 --shim ../../runtime/syslib/shim.c \
@@ -141,7 +161,7 @@ else
   no "gm2-built M2make drives m2 build"
   no "gm2-built M2make drives m2 build"
 fi
 fi
 
 
-rm -f Main MainX Main2 SepMain *.o
+rm -f Main MainX Main2 SepMain CDefProg *.o
 rm -rf gen_ssa
 rm -rf gen_ssa
 rm -f m2make.log
 rm -f m2make.log