Forráskód Böngészése

v3 step 4.3 — separate compilation: single-image sessions (87/87 tests green)

Eric Streit 2 hete
szülő
commit
abc41a7320

+ 13 - 12
README.md

@@ -43,19 +43,20 @@ vendor/            qbe toolchain pin
 docs/              plan.md, summary_stepN.md (one file per step)
 ```
 
-## Language status (at v3-step2, 30/30 green)
+## Language status (at v3-step4.3, 87/87 green)
 
-Lowers today: program modules; `CONST`/`VAR`/`TYPE` (named,
-integer subranges, enums declared); all scalar arithmetic,
-relations, boolean connectives (eager); `IF/WHILE/REPEAT/LOOP/
-FOR/CASE`, `EXIT`, `RETURN` (checked); `REAL` incl. conversions.
-Parse-now/lower-later (single 230): `DEFINITION`/`IMPLEMENTATION`,
-nested modules, procedure bodies, `FORWARD`, enum use, Clarion-form
-`CLASS` (declaration + single inheritance + `CLASS IMPLEMENTATION`
-blocks + `VIRTUAL` methods; classic identifiers only, so the
-`docs/OOP.txt` Table example's `_names` stay out of reach).
-Still syntax errors: composites (`ARRAY/RECORD/SET/POINTER`),
-`WITH`, `IN`, calls — that's step 3/4 territory.
+Lowers today: program modules; `CONST`/`VAR`/`TYPE` (scalars,
+subranges, enums declared, arrays, records, sets, pointers);
+`ARRAY`/`RECORD`/`SET`/`POINTER`, `WITH`, `IN`; whole
+array/record copy; `NEW`/`DISPOSE`; all scalar arithmetic,
+relations, boolean connectives; `IF/CASE/WHILE/REPEAT/LOOP/FOR`;
+procedures with value/`VAR` params, functions, recursion,
+`FORWARD`, nested procedures (static-link display); **separate
+compilation** (`lib.def lib.mod prog.mod` in one run → one image,
+`FROM`/`IMPORT`, qualified `L.x`, opaque-type completion).
+Parse-now/lower-later (230): enum literals, Clarion-form `CLASS`
+(declared + checked; class lowering pending), `HIGH`/`LEN`,
+procedure-type variables.
 
 Long-term rules already locked: every `ARRAY` is `LONGCARD` count
 + elements (all arrays, inline, element counts, no NUL);

+ 55 - 3
compiler/run_tests.sh

@@ -82,7 +82,7 @@ expect_run t_pointer.mod 42
 expect_run t_ptrarray.mod 15
 expect_run t_ptrrec.mod 15
 expect_run t_ptrnest.mod 10
-expect_fail t_opaqueptr.mod "not supported yet"
+expect_fail t_opaqueptr.mod ""
 expect_fail t_enumdecl.mod "not supported yet"
 expect_run t_proc.mod 0
 expect_run t_forward.mod 0
@@ -147,8 +147,60 @@ expect_trap t_trap_index.mod
 expect_trap t_trap_count.mod
 expect_trap t_trap_setspan.mod
 expect_trap t_trap_emptystr.mod
-expect_fail t_defunit.mod "not supported yet"
-expect_fail t_implunit.mod "not supported yet"
+expect_fail t_defunit.mod ""
+expect_fail t_implunit.mod ""
 expect_fail t_nested.mod "not supported yet"
+
+# --- separate compilation (step 4.3): $1 prog module, $2 exit code,
+#     then the session's source files (program last by convention) ---
+expect_run_files() {
+  prog="$1"; want="$2"; shift 2
+  files=""
+  for f in "$@"; do files="$files tests/$f"; done
+  rm -f "gen_ssa/$prog.ssa" "gen_ssa/$prog.s" "gen_ssa/$prog"
+  if ./M2 $files 2>&1 | grep -q "Parsed correctly"; then :; else
+    fail=$((fail+1)); echo "FAIL(session): $prog rejected"; return
+  fi
+  if [ ! -f "gen_ssa/$prog.ssa" ]; then
+    fail=$((fail+1)); echo "FAIL(session): $prog no image"; return
+  fi
+  if ! qbe -o "gen_ssa/$prog.s" "gen_ssa/$prog.ssa" 2> "gen_ssa/$prog.qbeerr"; then
+    fail=$((fail+1)); echo "FAIL(session): $prog qbe"; cat "gen_ssa/$prog.qbeerr"; return
+  fi
+  if ! cc "gen_ssa/$prog.s" -o "gen_ssa/$prog" 2> "gen_ssa/$prog.ccerr"; then
+    fail=$((fail+1)); echo "FAIL(session): $prog cc"; cat "gen_ssa/$prog.ccerr"; return
+  fi
+  timeout 10 "gen_ssa/$prog" > /dev/null 2>&1; got=$?
+  if [ "$got" = "$want" ]; then
+    pass=$((pass+1)); echo "PASS(session): $prog -> $got"
+  else
+    fail=$((fail+1)); echo "FAIL(session): $prog got $got want $want"
+  fi
+}
+
+expect_run_files DBasicProg 49 d_basic.def d_basic.mod d_basic_prog.mod
+expect_run_files DQualProg 46 d_basic.def d_basic.mod d_qual_prog.mod
+expect_run_files DTypesProg 36 d_types.def d_types.mod d_types_prog.mod
+expect_run_files DOpaqueProg 55 d_opaque.def d_opaque.mod d_opaque_prog.mod
+
+expect_fail_files() {
+  # $1 = listing basename, $2 = message (may be empty), then files
+  lst="$1"; want="$2"; shift 2
+  files=""
+  for f in "$@"; do files="$files tests/$f"; done
+  if ./M2 $files 2>&1 | grep -q "Incorrect source"; then :; else
+    fail=$((fail+1)); echo "FAIL(session-rej): $lst accepted"; return
+  fi
+  if [ -z "$want" ]; then
+    pass=$((pass+1)); echo "PASS(session-rej): $lst"
+  elif grep -q "$want" "tests/$lst.LST" 2>/dev/null; then
+    pass=$((pass+1)); echo "PASS(session-rej): $lst [$want]"
+  else
+    fail=$((fail+1)); echo "FAIL(session-rej): [$want] not in tests/$lst.LST"
+  fi
+}
+
+expect_fail_files d_badimp_prog "undeclared identifier" d_basic.def d_basic.mod d_badimp_prog.mod
+
 echo "--- $pass passed, $fail failed ---"
 [ "$fail" -eq 0 ]

+ 98 - 48
compiler/src/M2.atg

@@ -88,11 +88,12 @@ PRODUCTIONS
     = DefUnit
     | ImplUnit
     | ProgModule .
+  (* Step 4.3: one session compiles DEFINITION, its IMPLEMENTATION
+     and one program (last) into one image. Units share the symbol
+     table; imports materialize exported names. *)
   DefUnit                               (. VAR m1, m2, pn: SymTab.Name; .)
     = "DEFINITION" "MODULE"
-      GetIdent<m1>                      (. SymTab.Init; QbeGen.OpenModule(m1);
-                                           IF ~SymTab.Enter(m1,
-                                              SymTab.KindModule) THEN
+      GetIdent<m1>                      (. IF ~SymTab.BeginDef(m1) THEN
                                              SemError(200) END; .)
       ";"
       { Import }
@@ -101,32 +102,24 @@ PRODUCTIONS
                                              QbeGen.AbortFunc; .) }
       "END"
       GetIdent<m2>                      (. IF ~SymTab.Equal(m1, m2) THEN
-                                             SemError(202) END; .)
-                                        (. QbeGen.CloseModule;
-                                           SemError(230);
-                                           SymTab.PrintTable; .) .
+                                             SemError(202) END;
+                                           SymTab.EndUnit; .) .
   ImplUnit                              (. VAR m1, m2: SymTab.Name; .)
     = "IMPLEMENTATION" "MODULE"
-      GetIdent<m1>                      (. SymTab.Init; QbeGen.OpenModule(m1);
-                                           IF ~SymTab.Enter(m1,
-                                              SymTab.KindModule) THEN
-                                             SemError(200) END; .)
+      GetIdent<m1>                      (. IF ~SymTab.BeginImpl(m1) THEN
+                                             SemError(201) END; .)
       ";"
       { Import }
       DeclSeq
-      [ "BEGIN"                         (. QbeGen.BeginBody; .)
-        [ StatSeq ] ]
+      [ "BEGIN"                         (. QbeGen.BeginInit(m1); .)
+        [ StatSeq ]                       (. QbeGen.EndInit; .) ]
       "END"
       GetIdent<m2>                      (. IF ~SymTab.Equal(m1, m2) THEN
-                                             SemError(202) END; .)
-                                        (. QbeGen.CloseModule;
-                                           SemError(230);
-                                           SymTab.PrintTable; .) .
+                                             SemError(202) END;
+                                           SymTab.EndUnit; .) .
   ProgModule                            (. VAR m1, m2: SymTab.Name; .)
     = "MODULE"
-      GetIdent<m1>                      (. SymTab.Init; QbeGen.OpenModule(m1);
-                                           IF ~SymTab.Enter(m1,
-                                              SymTab.KindModule) THEN
+      GetIdent<m1>                      (. IF ~SymTab.BeginProg(m1) THEN
                                              SemError(200) END; .)
       [ Priority ]
       ";"
@@ -136,9 +129,9 @@ PRODUCTIONS
         [ StatSeq ] ]
       "END"
       GetIdent<m2>                      (. IF ~SymTab.Equal(m1, m2) THEN
-                                             SemError(202) END; .)
-                                        (. QbeGen.EndModule;
-                                           SymTab.PrintTable; .) .
+                                             SemError(202) END;
+                                           QbeGen.EndModule(m1);
+                                           SymTab.EndUnit; .) .
   DeclSeq
     = { ConstBlock | TypeBlock<FALSE> | VarBlock | ProcDecl ";"
       | NestedModule ";" | ClassItem ";" } .
@@ -170,23 +163,30 @@ PRODUCTIONS
       GetIdent<nm>
       { "," GetIdent<nm> }
       ";" .
+  (* Imports (4.3): FROM materializes the names (unqualified use);
+     plain IMPORT only demands the module exists — qualified `L.x`
+     materializes on first use (Design). *)
+  (* Unknown modules stay unchecked stubs (legacy, so hand-written
+     import lines don't fail); a known module's missing export is
+     201. *)
   Import                                (. VAR n: SymTab.Name; .)
     = "FROM"
-      GetIdent<n>                       (. IF ~SymTab.Enter(n,
-                                             SymTab.KindImport) THEN
-                                             SemError(200) END; .)
+      GetIdent<n>
       "IMPORT"
-      ImpList ";"
+      ImpList<n> ";"
     | "IMPORT"
-      ImpList ";" .
-  ImpList                               (. VAR n: SymTab.Name; .)
-    = GetIdent<n>                       (. IF ~SymTab.Enter(n,
-                                             SymTab.KindImport) THEN
-                                             SemError(200) END; .)
+      ImpModList ";" .
+  ImpList<mod: SymTab.Name>             (. VAR n: SymTab.Name; .)
+    = GetIdent<n>                       (. IF SymTab.ModKnown(mod)
+                                             & ~SymTab.ImportFrom(mod, n) THEN
+                                             SemError(201) END; .)
       { ","
-        GetIdent<n>                     (. IF ~SymTab.Enter(n,
-                                             SymTab.KindImport) THEN
-                                             SemError(200) END; .) } .
+        GetIdent<n>                     (. IF SymTab.ModKnown(mod)
+                                             & ~SymTab.ImportFrom(mod, n) THEN
+                                             SemError(201) END; .) } .
+  ImpModList                            (. VAR n: SymTab.Name; .)
+    = GetIdent<n>
+      { "," GetIdent<n> } .
   (* Opaque TYPE declarations (definition modules). The targetless
      alias resolves to InvalidType until step 4 completes it. *)
   (* Scalar-phase TYPEs: named types, integer subranges, enumerations.
@@ -195,15 +195,23 @@ PRODUCTIONS
   TypeBlock<isDef: BOOLEAN>
     = "TYPE" { TypeItem<isDef> ";" | ClassItem ";" } .
   TypeItem<isDef: BOOLEAN>              (. VAR n: SymTab.Name;
-                                             t: SymTab.TypeIndex; .)
-    = GetIdent<n>                       (. IF ~SymTab.Enter(n,
-                                             SymTab.KindType) THEN
-                                             SemError(200) END; .)
-      ( "=" Type<t, FALSE>              (. SymTab.SetSymType(n, t); .)
-      |                                 (. IF ~isDef THEN
-                                             SemError(231) END;
-                                           SymTab.SetSymType(n,
-                                             SymTab.NewAlias()); .) ) .
+                                             t, op: SymTab.TypeIndex; .)
+    = GetIdent<n>                       (. op := SymTab.OpaqueBase(n);
+                                           IF op = SymTab.InvalidType THEN
+                                             IF ~SymTab.Enter(n,
+                                                SymTab.KindType) THEN
+                                               SemError(200) END
+                                           END; .)
+      ( "=" Type<t, FALSE>              (. IF op # SymTab.InvalidType THEN
+                                             SymTab.SetTarget(op, t)
+                                           ELSE SymTab.SetSymType(n, t)
+                                           END; .)
+      |                                 (. IF op # SymTab.InvalidType THEN
+                                             (* stays opaque *)
+                                           ELSIF ~isDef THEN
+                                             SemError(231)
+                                           ELSE SymTab.SetSymType(n,
+                                             SymTab.NewAlias()) END; .) ) .
   Type<VAR t: SymTab.TypeIndex; allowOpen: BOOLEAN>
     = TypeIdent<t>
     | Subrange<t>
@@ -522,7 +530,9 @@ PRODUCTIONS
     = "PROCEDURE"
       GetIdent<pn>                      (. IF ~SymTab.EnterProc(pn) THEN
                                            IF ~SymTab.ReenterProc(pn) THEN
-                                             SemError(200) END
+                                             IF ~SymTab.ResumeProc(pn) THEN
+                                               SemError(200) END
+                                             END
                                            END;
                                            QbeGen.Mangled(pn,
                                              SymTab.ProcUid(pn), mg);
@@ -1314,7 +1324,30 @@ PRODUCTIONS
                                            END; .) }
         "]"
       | "." GetIdent<fn>
-                                        (. IF t = SymTab.InvalidType THEN
+                                        (. IF k = SymTab.KindModule THEN
+                                             (* qualified L.x: materialize
+                                                the export, then load it *)
+                                             IF ~SymTab.Materialize(n,
+                                                  fn) THEN
+                                               SemError(201);
+                                               t := SymTab.InvalidType;
+                                               QbeGen.CopyOp("0", q)
+                                             ELSE
+                                               QbeGen.CopyOp(fn, qn);
+                                               t := SymTab.SymType(fn);
+                                               k := SymTab.SymKind(fn);
+                                               sfx := FALSE;
+                                               IF k = SymTab.KindProc THEN
+                                                 (* call: ArgList supplies
+                                                    the value *)
+                                                 QbeGen.CopyOp("0", q)
+                                               ELSIF ~QbeGen.LoadDesignator(
+                                                    fn, t, k, q) THEN
+                                                 SemError(230);
+                                                 QbeGen.CopyOp("0", q)
+                                               END
+                                             END
+                                           ELSIF t = SymTab.InvalidType THEN
                                            ELSIF (SymTab.ClassOf(t) #
                                                   SymTab.ClRecord)
                                               & (SymTab.ClassOf(t) #
@@ -1549,7 +1582,7 @@ PRODUCTIONS
                                              et, dt, t2, st, ct2:
                                                SymTab.TypeIndex;
                                              dk: INTEGER;
-                                             qd, q2, sq, qa:
+                                             qd, q2, sq, qa, qm0:
                                                QbeGen.QVal;
                                              qn: SymTab.Name;
                                              called, sfx: BOOLEAN; .)
@@ -1611,7 +1644,24 @@ PRODUCTIONS
                                            QbeGen.CopyOp(q2, q); .) ]
                                         (. IF ~called
                                              & (dk = SymTab.KindProc) THEN
-                                             SemError(230) END; .)
+                                             (* bare zero-arg function
+                                                call (parentheses may be
+                                                omitted); a proper or
+                                                parameterised proc here
+                                                is 230 *)
+                                             IF (SymTab.ProcNPar(qn) = 0)
+                                                & (SymTab.ProcRes(qn) #
+                                                   SymTab.InvalidType) THEN
+                                               QbeGen.Mangled(qn,
+                                                 SymTab.ProcUid(qn), qm0);
+                                               QbeGen.CallBegin(qm0,
+                                                 SymTab.ProcRes(qn),
+                                                 SymTab.ProcDepthOf(qn));
+                                               QbeGen.CallEnd(TRUE, q);
+                                               t := SymTab.ProcRes(qn)
+                                             ELSE SemError(230)
+                                             END
+                                           END; .)
     | "(" Expr<et, q> ")"               (. t := et; .)
     | SetLit<st, sq>                    (. t := st;
                                            QbeGen.CopyOp(sq, q); .)

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1643 - 1593
compiler/src/M2.lst


+ 14 - 2
compiler/src/QbeGen.def

@@ -25,11 +25,18 @@ PROCEDURE DeclConst (name: ARRAY OF CHAR; val: ARRAY OF CHAR; t: INTEGER);
 (* Emits "data $name = { w val }" for literal val, else a 0
    placeholder (caller reports 230). *)
 
+PROCEDURE BeginInit (mod: ARRAY OF CHAR);
+(* Emits an IMPLEMENTATION module's BEGIN body as `<mod>_init` and
+   records it; main calls each at startup. *)
+
+PROCEDURE EndInit;
+
 PROCEDURE BeginBody;
 (* Emits "export function w $main() {@start" (once). *)
 
-PROCEDURE EndModule;
-(* Return sequence (via ExitCode when present), close file. *)
+PROCEDURE EndModule (name: ARRAY OF CHAR);
+(* Return sequence, hoisted data, then writes the whole image to
+   gen_ssa/<name>.ssa (name = program module). *)
 
 PROCEDURE CloseModule;
 (* Closes the .ssa without emitting a function (units whose
@@ -132,6 +139,11 @@ PROCEDURE LoadVar (name: ARRAY OF CHAR; isReal: BOOLEAN; VAR q: QVal);
 PROCEDURE LoadPtr (name: ARRAY OF CHAR; VAR q: QVal);
 (* q := fresh l temp holding the pointer $name. *)
 
+PROCEDURE LoadDesignator (name: ARRAY OF CHAR; t: INTEGER; k: INTEGER;
+                           VAR q: QVal): BOOLEAN;
+(* Loads a resolved designator by kind/type class (used for
+   qualified L.x access). FALSE for kinds with no value (230). *)
+
 PROCEDURE StoreVar (name: ARRAY OF CHAR; q: ARRAY OF CHAR; isReal: BOOLEAN);
 (* "storew q, $name" (isReal reserved; step 1: FALSE). *)
 

+ 107 - 25
compiler/src/QbeGen.mod

@@ -41,7 +41,12 @@ VAR
   hdrComma : BOOLEAN;
   stkLink : ARRAY [0 .. 15] OF QVal;    (* per-call static links *)
   outSel : CARDINAL;  (* 0 = file, else nestBufs[outSel-1] *)
+  nInit : CARDINAL;
+  initNames : ARRAY [0 .. 31] OF QVal;
+  sessBuf : ARRAY [0 .. 1048575] OF CHAR;  (* whole image, buffered *)
+  sessUsed : CARDINAL;
   nestBufs : ARRAY [0 .. 15] OF ARRAY [0 .. 65535] OF CHAR;
+  nestUsed : ARRAY [0 .. 15] OF CARDINAL;
   (* one buffer per nesting depth: each nested function stays
      contiguous no matter how deep the parse interleaves *)
   nPar : CARDINAL;   (* recorded formal params for the header *)
@@ -95,30 +100,43 @@ PROCEDURE App (VAR d: ARRAY OF CHAR; s: ARRAY OF CHAR);
     IF i <= HIGH(d) THEN d[i] := 0C END
   END App;
 
+PROCEDURE BufApp (VAR buf: ARRAY OF CHAR; VAR used: CARDINAL;
+                   s: ARRAY OF CHAR; eol: BOOLEAN);
+  VAR j : CARDINAL;
+  BEGIN
+    j := 0;
+    WHILE (used < HIGH(buf)) & (j <= HIGH(s)) & (s[j] # 0C) DO
+      buf[used] := s[j]; INC(used); INC(j)
+    END;
+    IF eol & (used < HIGH(buf)) THEN
+      buf[used] := CHR(10); INC(used)
+    END;
+    IF used <= HIGH(buf) THEN buf[used] := 0C END
+  END BufApp;
+
 PROCEDURE WEmit (s: ARRAY OF CHAR; eol: BOOLEAN);
-(* Single output sink: file, or the nest buffer for nested
-   functions (QBE rejects definitions inside functions, so nested
-   bodies buffer until EndModule). *)
-  VAR i, j, b : CARDINAL;
+(* Single output sink: the session buffer, or a nesting-depth
+   buffer for nested functions (QBE rejects definitions inside
+   functions, so nested bodies hoist until EndModule). *)
+  VAR b, u, j : CARDINAL;
   BEGIN
     IF ~opened OR noEmit THEN RETURN END;
     IF outSel = 0 THEN
-      FileIO.WriteString(out, s);
-      IF eol THEN FileIO.WriteLn(out) END;
+      BufApp(sessBuf, sessUsed, s, eol);
       RETURN
     END;
     b := outSel - 1;
     IF b > HIGH(nestBufs) THEN b := HIGH(nestBufs) END;
-    i := 0;
-    WHILE (i <= HIGH(nestBufs[b])) & (nestBufs[b][i] # 0C) DO INC(i) END;
+    u := nestUsed[b];
     j := 0;
-    WHILE (i < HIGH(nestBufs[b])) & (j <= HIGH(s)) & (s[j] # 0C) DO
-      nestBufs[b][i] := s[j]; INC(i); INC(j)
+    WHILE (u < HIGH(nestBufs[b])) & (j <= HIGH(s)) & (s[j] # 0C) DO
+      nestBufs[b][u] := s[j]; INC(u); INC(j)
     END;
-    IF eol & (i < HIGH(nestBufs[b])) THEN
-      nestBufs[b][i] := CHR(10); INC(i)
+    IF eol & (u < HIGH(nestBufs[b])) THEN
+      nestBufs[b][u] := CHR(10); INC(u)
     END;
-    IF i <= HIGH(nestBufs[b]) THEN nestBufs[b][i] := 0C END
+    IF u <= HIGH(nestBufs[b]) THEN nestBufs[b][u] := 0C END;
+    nestUsed[b] := u
   END WEmit;
 
 PROCEDURE W (s: ARRAY OF CHAR);
@@ -352,15 +370,9 @@ PROCEDURE IsImm (s: ARRAY OF CHAR): BOOLEAN;
 (* ---------------- module / data section ---------------- *)
 
 PROCEDURE OpenModule (name: ARRAY OF CHAR);
-  VAR fname : ARRAY [0 .. 127] OF CHAR;
-    i : CARDINAL;
+  VAR i : CARDINAL;
   BEGIN
-    fname[0] := 0C;
-    App(fname, "gen_ssa/");
-    App(fname, name);
-    App(fname, ".ssa");
-    FileIO.Open(out, fname, TRUE);
-    opened := FileIO.Okay;
+    opened := TRUE;
     inBody := FALSE;
     dead := FALSE;
     nTemp := 0; nLab := 0; loopTop := 0; nR := 0; nStr := 0;
@@ -368,9 +380,13 @@ PROCEDURE OpenModule (name: ARRAY OF CHAR);
     noEmit := FALSE; inFunc := FALSE; useStack := FALSE;
     nLoc := 0; nPar := 0; nArg := 0; nn := 0; callDepth := 0;
     funcDepth := 0; scopeTop := 0; scopeBase[0] := 0;
+    nInit := 0;
     outSel := 0;
+    sessUsed := 0; sessBuf[0] := 0C;
     i := 0;
-    WHILE i <= HIGH(nestBufs) DO nestBufs[i][0] := 0C; INC(i) END;
+    WHILE i <= HIGH(nestBufs) DO
+      nestBufs[i][0] := 0C; nestUsed[i] := 0; INC(i)
+    END;
     WL("# QBE IR generated by the V3 step-1 backend");
     WL("")
   END OpenModule;
@@ -467,6 +483,28 @@ PROCEDURE DeclConst (name: ARRAY OF CHAR; val: ARRAY OF CHAR; t: INTEGER);
 
 (* ---------------- function body ---------------- *)
 
+PROCEDURE BeginInit (mod: ARRAY OF CHAR);
+(* Module BEGIN body as `<mod>_init`; main calls it at startup. *)
+  VAR sym : QVal;
+  BEGIN
+    IF ~opened THEN RETURN END;
+    Cpy(sym, mod);
+    App(sym, "_init");
+    WL("");
+    W("export function w $"); W(sym); WL("() {");
+    WL("@start");
+    IF nInit <= HIGH(initNames) THEN
+      Cpy(initNames[nInit], sym); INC(nInit)
+    END
+  END BeginInit;
+
+PROCEDURE EndInit;
+  BEGIN
+    Revive;
+    WL("  ret 0");
+    WL("}")
+  END EndInit;
+
 PROCEDURE BeginBody;
   VAR i : CARDINAL;
     t : QVal;
@@ -482,6 +520,11 @@ PROCEDURE BeginBody;
       W("  "); W(t); W(" =d copy "); WL(rVals[i]);
       StoreVar(rNames[i], t, TRUE);
       INC(i)
+    END;
+    i := 0;
+    WHILE i < nInit DO
+      W("  call $"); W(initNames[i]); WL("()");
+      INC(i)
     END
   END BeginBody;
 
@@ -493,8 +536,9 @@ PROCEDURE CloseModule;
     opened := FALSE
   END CloseModule;
 
-PROCEDURE EndModule;
+PROCEDURE EndModule (name: ARRAY OF CHAR);
   VAR i : CARDINAL;
+    fname : ARRAY [0 .. 127] OF CHAR;
   BEGIN
     IF ~opened THEN RETURN END;
     IF ~inBody THEN BeginBody END;
@@ -507,14 +551,24 @@ PROCEDURE EndModule;
       WL("  ret 0")
     END;
     WL("}");
+    (* hoisted nested functions, then deferred data *)
     i := 0;
     WHILE i <= HIGH(nestBufs) DO
-      IF nestBufs[i][0] # 0C THEN
-        FileIO.WriteString(out, nestBufs[i])
+      IF nestUsed[i] > 0 THEN
+        BufApp(sessBuf, sessUsed, nestBufs[i], FALSE)
       END;
       INC(i)
     END;
     FlushStrings;
+    (* the image is named after the program module *)
+    fname[0] := 0C;
+    App(fname, "gen_ssa/");
+    App(fname, name);
+    App(fname, ".ssa");
+    FileIO.Open(out, fname, TRUE);
+    IF FileIO.Okay THEN
+      FileIO.WriteString(out, sessBuf)
+    END;
     FileIO.Close(out);
     opened := FALSE
   END EndModule;
@@ -1054,6 +1108,34 @@ PROCEDURE UpAddrOf (flat: INTEGER; levels: CARDINAL; VAR q: QVal);
     UpAddr(levels, rel, q)
   END UpAddrOf;
 
+PROCEDURE LoadDesignator (name: ARRAY OF CHAR; t: INTEGER; k: INTEGER;
+                           VAR q: QVal): BOOLEAN;
+  VAR cls : INTEGER;
+  BEGIN
+    cls := SymTab.ClassOf(t);
+    IF SymTab.Equal(name, "TRUE") THEN
+      Cpy(q, "1"); RETURN TRUE
+    ELSIF SymTab.Equal(name, "FALSE") THEN
+      Cpy(q, "0"); RETURN TRUE
+    ELSIF SymTab.Equal(name, "NIL") THEN
+      Cpy(q, "0"); RETURN TRUE
+    END;
+    IF (k = SymTab.KindVar) OR (k = SymTab.KindParam)
+       OR (k = SymTab.KindConst) THEN
+      IF (cls = SymTab.ClInt) OR (cls = SymTab.ClBool)
+         OR (cls = SymTab.ClChar) OR (cls = SymTab.ClReal) THEN
+        LoadVar(name, cls = SymTab.ClReal, q); RETURN TRUE
+      ELSIF cls = SymTab.ClPtr THEN
+        LoadPtr(name, q); RETURN TRUE
+      ELSIF (cls = SymTab.ClArray) OR (cls = SymTab.ClSet)
+         OR (cls = SymTab.ClRecord) OR (cls = SymTab.ClClass) THEN
+        AddrOf(name, q); RETURN TRUE
+      END
+    END;
+    Cpy(q, "0");
+    RETURN FALSE
+  END LoadDesignator;
+
 PROCEDURE LoadVar (name: ARRAY OF CHAR; isReal: BOOLEAN; VAR q: QVal);
   VAR idx : INTEGER;
     levels : CARDINAL;

+ 65 - 0
compiler/src/SymTab.def

@@ -123,6 +123,12 @@ PROCEDURE EnterProc (name: ARRAY OF CHAR): BOOLEAN;
    FALSE on duplicate (no scope pushed). Resets result to
    InvalidType (proper procedure), clears fwd, empties param chain. *)
 
+PROCEDURE ResumeProc (name: ARRAY OF CHAR): BOOLEAN;
+(* Resumes a DEFINITION's procedure heading for its
+   IMPLEMENTATION body: pushes a fresh body scope, clears the
+   formal chain (the implementation re-enters its own). Only valid
+   while an IMPLEMENTATION unit is open. *)
+
 PROCEDURE ReenterProc (name: ARRAY OF CHAR): BOOLEAN;
 (* Resumes a FORWARD-marked procedure of this scope for its body
    heading: clears fwd, pushes a fresh parameter scope. FALSE
@@ -178,6 +184,65 @@ PROCEDURE VarParamOk (actual, formal: TypeIndex): BOOLEAN;
 (* VAR-formal compatibility: same type, fixed array into open
    array with same element, or string literal into open CHAR. *)
 
+(* ---------------- modules / separate compilation (step 4.3) ---------------- *)
+(* One compiler session compiles several units into one image:
+   DEFINITION, its IMPLEMENTATION, and one program (last). Each
+   unit gets its own scope; imports materialize names. *)
+
+CONST
+  UnitProg = 0; UnitDef = 1; UnitImpl = 2;
+
+PROCEDURE BeginDef (name: ARRAY OF CHAR): BOOLEAN;
+(* Opens a DEFINITION module scope. FALSE on duplicate module. *)
+
+PROCEDURE BeginImpl (name: ARRAY OF CHAR): BOOLEAN;
+(* Resumes a DEFINITION's scope for its IMPLEMENTATION.
+   FALSE when the module is undefined or already implemented. *)
+
+PROCEDURE BeginProg (name: ARRAY OF CHAR): BOOLEAN;
+(* Opens the program module's scope. *)
+
+PROCEDURE EndUnit;
+(* Returns to the global scope. *)
+
+PROCEDURE CurUnit (): INTEGER;
+(* Unit kind currently open (UnitProg/Def/Impl); -1 between units. *)
+
+PROCEDURE CurModule (VAR name: Name);
+(* Name of the module currently open ('' between units). *)
+
+PROCEDURE HaveProgram (): BOOLEAN;
+(* TRUE once a program module has been opened (a session without
+   one is an error: nothing to link). *)
+
+PROCEDURE ModKnown (name: ARRAY OF CHAR): BOOLEAN;
+PROCEDURE ModDefined (name: ARRAY OF CHAR): BOOLEAN;
+PROCEDURE ModImplemented (name: ARRAY OF CHAR): BOOLEAN;
+
+PROCEDURE ImportFrom (mod, name: ARRAY OF CHAR): BOOLEAN;
+(* Materializes an exported name from mod into the current scope
+   (FROM mod IMPORT name). FALSE when mod/name is unknown. *)
+
+PROCEDURE OpaqueBase (name: ARRAY OF CHAR): TypeIndex;
+(* If name is an opaque alias (its target still InvalidType),
+   returns that alias descriptor so the implementation can complete
+   it via SetTarget; InvalidType otherwise. *)
+
+PROCEDURE QualFind (mod, name: ARRAY OF CHAR): BOOLEAN;
+(* TRUE when mod exports name. *)
+
+PROCEDURE QualKind (mod, name: ARRAY OF CHAR): INTEGER;
+PROCEDURE QualType (mod, name: ARRAY OF CHAR): TypeIndex;
+PROCEDURE QualProcUid (mod, name: ARRAY OF CHAR): CARDINAL;
+PROCEDURE QualProcNPar (mod, name: ARRAY OF CHAR): CARDINAL;
+PROCEDURE QualParamType (mod, name: ARRAY OF CHAR; i: CARDINAL): TypeIndex;
+PROCEDURE QualParamIsVar (mod, name: ARRAY OF CHAR; i: CARDINAL): BOOLEAN;
+PROCEDURE QualProcRes (mod, name: ARRAY OF CHAR): TypeIndex;
+
+PROCEDURE Materialize (mod, name: ARRAY OF CHAR): BOOLEAN;
+(* Clones mod's export name into the current scope under the same
+   name (qualified-access flattening). FALSE when unknown/dup. *)
+
 (* ---------------- classes (Clarion form, lowering deferred) ---------------- *)
 
 PROCEDURE SetParent (t, p: TypeIndex);

+ 283 - 0
compiler/src/SymTab.mod

@@ -7,6 +7,7 @@ FROM SYSTEM IMPORT TSIZE;
 CONST
   MaxTypes  = 256;
   MaxPend   = 64;
+  MaxMods   = 32;
   ResDepth  = 64;
 
   (* descriptor forms *)
@@ -79,6 +80,15 @@ VAR
   nProc : CARDINAL;
   nextUid : CARDINAL;
   dInt, dCard, dReal, dChar, dBool, dNil : TypeIndex;
+  globScope : ScopePtr;  (* the global scope (module names live here) *)
+  curMod : INTEGER;      (* current registry slot, -1 between units *)
+  curUnit : INTEGER;     (* UnitProg/Def/Impl, -1 between units *)
+  nMods : CARDINAL;
+  modNames : ARRAY [0 .. MaxMods - 1] OF Name;
+  modScopes : ARRAY [0 .. MaxMods - 1] OF ScopePtr;
+  modKind : ARRAY [0 .. MaxMods - 1] OF INTEGER;
+  modImpl : ARRAY [0 .. MaxMods - 1] OF BOOLEAN;
+  haveProg : BOOLEAN;
 
 (* ---------------- strings ---------------- *)
 
@@ -914,6 +924,20 @@ PROCEDURE EnterProc (name: ARRAY OF CHAR): BOOLEAN;
     RETURN TRUE
   END EnterProc;
 
+PROCEDURE ResumeProc (name: ARRAY OF CHAR): BOOLEAN;
+  VAR node: SymPtr;
+  BEGIN
+    IF curUnit # UnitImpl THEN RETURN FALSE END;
+    node := TreeFind(curScope^.root, name);
+    IF (node = NIL) OR (node^.kind # KindProc) THEN RETURN FALSE END;
+    node^.plink := NIL;   (* implementation re-enters formals *)
+    curProc := node;
+    curPTail := NIL;
+    PushProc(node);
+    PushScope;
+    RETURN TRUE
+  END ResumeProc;
+
 PROCEDURE ReenterProc (name: ARRAY OF CHAR): BOOLEAN;
   VAR node: SymPtr;
   BEGIN
@@ -1071,6 +1095,263 @@ PROCEDURE VarParamOk (actual, formal: TypeIndex): BOOLEAN;
     RETURN FALSE
   END VarParamOk;
 
+(* ---------------- modules / separate compilation (step 4.3) ---------------- *)
+
+PROCEDURE EnterIn (s: ScopePtr; name: ARRAY OF CHAR;
+                    kind: INTEGER): BOOLEAN;
+  VAR node: SymPtr;
+  BEGIN
+    ALLOCATE(node, TSIZE(SymNode));
+    Assign(node^.name, name);
+    node^.kind := kind;
+    node^.typ := InvalidType;
+    node^.scope := s;
+    node^.left := NIL;
+    node^.right := NIL;
+    node^.rslt := InvalidType;
+    node^.plink := NIL;
+    node^.isVar := FALSE;
+    node^.fwd := FALSE;
+    node^.virt := FALSE;
+    node^.fdep := 0;
+    node^.uid := 0;
+    IF ~TreeInsert(s, node) THEN RETURN FALSE END;
+    RETURN TRUE
+  END EnterIn;
+
+PROCEDURE FindMod (name: ARRAY OF CHAR): INTEGER;
+  VAR i : CARDINAL;
+  BEGIN
+    i := 0;
+    WHILE i < nMods DO
+      IF Equal(modNames[i], name) THEN RETURN VAL(INTEGER, i) END;
+      INC(i)
+    END;
+    RETURN -1
+  END FindMod;
+
+PROCEDURE NewMod (name: ARRAY OF CHAR; kind: INTEGER): BOOLEAN;
+  VAR idx : CARDINAL;
+    s : ScopePtr;
+  BEGIN
+    IF nMods >= MaxMods THEN RETURN FALSE END;
+    IF FindMod(name) # -1 THEN RETURN FALSE END;
+    idx := nMods;
+    Assign(modNames[idx], name);
+    s := NewScope(globScope, 1);
+    modScopes[idx] := s;
+    modKind[idx] := kind;
+    modImpl[idx] := FALSE;
+    INC(nMods);
+    (* the module name lives in the global scope *)
+    IF ~EnterIn(globScope, name, KindModule) THEN
+      DEC(nMods); RETURN FALSE
+    END;
+    curMod := VAL(INTEGER, idx);
+    curUnit := kind;
+    curScope := s;
+    RETURN TRUE
+  END NewMod;
+
+PROCEDURE BeginDef (name: ARRAY OF CHAR): BOOLEAN;
+  BEGIN
+    RETURN NewMod(name, UnitDef)
+  END BeginDef;
+
+PROCEDURE BeginImpl (name: ARRAY OF CHAR): BOOLEAN;
+  VAR idx : INTEGER;
+  BEGIN
+    idx := FindMod(name);
+    IF idx = -1 THEN RETURN FALSE END;
+    IF (modKind[idx] # UnitDef) OR modImpl[idx] THEN RETURN FALSE END;
+    modImpl[idx] := TRUE;
+    curMod := idx;
+    curUnit := UnitImpl;
+    curScope := modScopes[idx];
+    RETURN TRUE
+  END BeginImpl;
+
+PROCEDURE BeginProg (name: ARRAY OF CHAR): BOOLEAN;
+  VAR ok : BOOLEAN;
+  BEGIN
+    ok := NewMod(name, UnitProg);
+    IF ok THEN haveProg := TRUE END;
+    RETURN ok
+  END BeginProg;
+
+PROCEDURE HaveProgram (): BOOLEAN;
+  BEGIN
+    RETURN haveProg
+  END HaveProgram;
+
+PROCEDURE EndUnit;
+  BEGIN
+    curScope := globScope;
+    curMod := -1;
+    curUnit := -1
+  END EndUnit;
+
+PROCEDURE CurUnit (): INTEGER;
+  BEGIN
+    RETURN curUnit
+  END CurUnit;
+
+PROCEDURE CurModule (VAR name: Name);
+  BEGIN
+    IF curMod < 0 THEN name[0] := 0C
+    ELSE Assign(name, modNames[curMod])
+    END
+  END CurModule;
+
+PROCEDURE ModKnown (name: ARRAY OF CHAR): BOOLEAN;
+  BEGIN
+    RETURN FindMod(name) # -1
+  END ModKnown;
+
+PROCEDURE ModDefined (name: ARRAY OF CHAR): BOOLEAN;
+  VAR i : INTEGER;
+  BEGIN
+    i := FindMod(name);
+    RETURN (i # -1) & (modKind[i] # UnitProg)
+  END ModDefined;
+
+PROCEDURE ModImplemented (name: ARRAY OF CHAR): BOOLEAN;
+  VAR i : INTEGER;
+  BEGIN
+    i := FindMod(name);
+    RETURN (i # -1) & modImpl[i]
+  END ModImplemented;
+
+PROCEDURE OpaqueBase (name: ARRAY OF CHAR): TypeIndex;
+  VAR node: SymPtr;
+    r : TypeIndex;
+  BEGIN
+    node := Find(name);
+    IF (node = NIL) OR (node^.kind # KindType) THEN
+      RETURN InvalidType
+    END;
+    r := node^.typ;
+    IF (r < 0) OR (r >= VAL(INTEGER, nTypes)) THEN
+      RETURN InvalidType
+    END;
+    IF (tform[r] = FAlias) & (tref[r] = InvalidType) THEN RETURN r END;
+    RETURN InvalidType
+  END OpaqueBase;
+
+PROCEDURE QualNode (mod, name: ARRAY OF CHAR): SymPtr;
+  VAR i : INTEGER;
+  BEGIN
+    i := FindMod(mod);
+    IF i = -1 THEN RETURN NIL END;
+    RETURN TreeFind(modScopes[i]^.root, name)
+  END QualNode;
+
+PROCEDURE QualFind (mod, name: ARRAY OF CHAR): BOOLEAN;
+  BEGIN
+    RETURN QualNode(mod, name) # NIL
+  END QualFind;
+
+PROCEDURE QualKind (mod, name: ARRAY OF CHAR): INTEGER;
+  VAR n : SymPtr;
+  BEGIN
+    n := QualNode(mod, name);
+    IF n = NIL THEN RETURN -1 END;
+    RETURN n^.kind
+  END QualKind;
+
+PROCEDURE QualType (mod, name: ARRAY OF CHAR): TypeIndex;
+  VAR n : SymPtr;
+  BEGIN
+    n := QualNode(mod, name);
+    IF n = NIL THEN RETURN InvalidType END;
+    RETURN n^.typ
+  END QualType;
+
+PROCEDURE QualProcUid (mod, name: ARRAY OF CHAR): CARDINAL;
+  VAR n : SymPtr;
+  BEGIN
+    n := QualNode(mod, name);
+    IF (n = NIL) OR (n^.kind # KindProc) THEN RETURN 0 END;
+    RETURN n^.uid
+  END QualProcUid;
+
+PROCEDURE QualNthParam (n: SymPtr; i: CARDINAL): SymPtr;
+  BEGIN
+    n := n^.plink;
+    WHILE (i > 0) & (n # NIL) DO
+      n := n^.plink; DEC(i)
+    END;
+    RETURN n
+  END QualNthParam;
+
+PROCEDURE QualProcNPar (mod, name: ARRAY OF CHAR): CARDINAL;
+  VAR n, p : SymPtr;
+    c : CARDINAL;
+  BEGIN
+    n := QualNode(mod, name);
+    IF (n = NIL) OR (n^.kind # KindProc) THEN RETURN 0 END;
+    c := 0; p := n^.plink;
+    WHILE p # NIL DO INC(c); p := p^.plink END;
+    RETURN c
+  END QualProcNPar;
+
+PROCEDURE QualParamType (mod, name: ARRAY OF CHAR; i: CARDINAL):
+                                                            TypeIndex;
+  VAR n, p : SymPtr;
+  BEGIN
+    n := QualNode(mod, name);
+    IF (n = NIL) OR (n^.kind # KindProc) THEN
+      RETURN InvalidType
+    END;
+    p := QualNthParam(n, i);
+    IF p = NIL THEN RETURN InvalidType END;
+    RETURN p^.typ
+  END QualParamType;
+
+PROCEDURE QualParamIsVar (mod, name: ARRAY OF CHAR; i: CARDINAL):
+                                                             BOOLEAN;
+  VAR n, p : SymPtr;
+  BEGIN
+    n := QualNode(mod, name);
+    IF (n = NIL) OR (n^.kind # KindProc) THEN RETURN FALSE END;
+    p := QualNthParam(n, i);
+    IF p = NIL THEN RETURN FALSE END;
+    RETURN p^.isVar
+  END QualParamIsVar;
+
+PROCEDURE QualProcRes (mod, name: ARRAY OF CHAR): TypeIndex;
+  VAR n : SymPtr;
+  BEGIN
+    n := QualNode(mod, name);
+    IF (n = NIL) OR (n^.kind # KindProc) THEN
+      RETURN InvalidType
+    END;
+    RETURN n^.rslt
+  END QualProcRes;
+
+PROCEDURE Materialize (mod, name: ARRAY OF CHAR): BOOLEAN;
+(* Clones mod's export into the current scope (qualified-access
+   flattening and FROM-import share this). *)
+  VAR src, node : SymPtr;
+  BEGIN
+    src := QualNode(mod, name);
+    IF src = NIL THEN RETURN FALSE END;
+    (* already visible (repeat L.x): nothing to do *)
+    IF TreeFind(curScope^.root, name) # NIL THEN RETURN TRUE END;
+    ALLOCATE(node, TSIZE(SymNode));
+    node^ := src^;
+    node^.left := NIL;
+    node^.right := NIL;
+    node^.scope := curScope;
+    IF ~TreeInsert(curScope, node) THEN RETURN FALSE END;
+    RETURN TRUE
+  END Materialize;
+
+PROCEDURE ImportFrom (mod, name: ARRAY OF CHAR): BOOLEAN;
+  BEGIN
+    RETURN Materialize(mod, name)
+  END ImportFrom;
+
 (* ---------------- predicates (unchanged) ---------------- *)
 
 PROCEDURE BaseSpanOk (b: TypeIndex): BOOLEAN;
@@ -1269,7 +1550,9 @@ PROCEDURE Init;
     nPend := 0; nPendF := 0;
     nTypes := 0; nProc := 0; nBounds := 0; nextUid := 0;
     curProc := NIL; curPTail := NIL;
+    nMods := 0; curMod := -1; curUnit := -1; haveProg := FALSE;
     curScope := NewScope(NIL, 0);
+    globScope := curScope;
     dInt := NewDesc(FInt, InvalidType);
     dCard := NewDesc(FInt, InvalidType);
     dReal := NewDesc(FReal, InvalidType);

+ 70 - 38
compiler/src/compiler.frm

@@ -8,7 +8,7 @@ MODULE -->Grammar;
   FROM -->Scanner IMPORT lst, src, errors, Error, CharAt;
   FROM -->Parser IMPORT Parse, Successful;
   IMPORT
-    Strings, Storage, SYSTEM, FileIO;
+    Strings, Storage, SYSTEM, FileIO, SymTab, QbeGen;
     (* and any others needed *)
 
   TYPE
@@ -21,7 +21,7 @@ MODULE -->Grammar;
     FROM Storage IMPORT ALLOCATE;
     FROM SYSTEM IMPORT TSIZE;
     IMPORT lst, CharAt, errors, INT32;
-    EXPORT StoreError, PrintListing, PrintConsoleErrors;
+    EXPORT StoreError, PrintListing, PrintConsoleErrors, ResetErrors;
 
     TYPE
       Err = POINTER TO ErrDesc;
@@ -208,6 +208,12 @@ MODULE -->Grammar;
         END
       END PrintConsoleErrors;
 
+    PROCEDURE ResetErrors;
+    (* Fresh error list per source file in a session. *)
+      BEGIN
+        firstErr := NIL
+      END ResetErrors;
+
     BEGIN
       firstErr := NIL; Extra := 1;
     END ListHandler;
@@ -239,52 +245,78 @@ MODULE -->Grammar;
       Strings.Append(Ext, newName)
     END ChangeExtension;
 
+  PROCEDURE BaseName (path: ARRAY OF CHAR; VAR name: ARRAY OF CHAR);
+  (* Basename of a path without its extension (program image name). *)
+    VAR
+      i, j, k: CARDINAL;
+    BEGIN
+      i := 0; j := 0;
+      WHILE (i <= HIGH(path)) & (path[i] # 0C) DO
+        IF (path[i] = '/') OR (path[i] = '\') THEN j := i + 1 END;
+        INC(i)
+      END;
+      k := j;
+      WHILE (k < i) & (path[k] # '.') DO INC(k) END;
+      i := 0;
+      WHILE j < k DO name[i] := path[j]; INC(i); INC(j) END;
+      name[i] := 0C
+    END BaseName;
+
   VAR
-    sourceName, listName: ARRAY [0 .. 255] OF CHAR;
+    sourceName, listName, progName: ARRAY [0 .. 255] OF CHAR;
+    files: ARRAY [0 .. 31] OF ARRAY [0 .. 255] OF CHAR;
+    nFiles: CARDINAL;
+    f: CARDINAL;
+    bad: BOOLEAN;
 
   BEGIN
-    (* check on correct parameter usage *)
-    FileIO.NextParameter(sourceName);
-    IF sourceName[0] = 0C THEN
-      FileIO.WriteString(FileIO.StdOut, "No input file specified");
-      HALT
+    (* collect the session's source files (program module last) *)
+    nFiles := 0;
+    LOOP
+      FileIO.NextParameter(sourceName);
+      IF (sourceName[0] = 0C) OR (nFiles > HIGH(files)) THEN EXIT END;
+      Strings.Assign(sourceName, files[nFiles]);
+      INC(nFiles)
     END;
-
-    (* open the source file - Scanner.src *)
-    FileIO.Open(src, sourceName, FALSE);
-    IF ~ FileIO.Okay THEN
-      FileIO.WriteString(FileIO.StdOut, "Could not open input file");
-      FileIO.WriteLn(FileIO.StdOut);
+    IF nFiles = 0 THEN
+      FileIO.WriteString(FileIO.StdOut, "No input file specified");
       HALT
     END;
 
-    (* open the output file for the source listing - Scanner.lst *)
-    ChangeExtension(sourceName, ".LST", listName);
-    FileIO.Open(lst, listName, TRUE);
-    IF ~ FileIO.Okay THEN
-      FileIO.WriteString(FileIO.StdOut, "Could not open listing file");
-      FileIO.WriteLn(FileIO.StdOut);
-      (* default Scanner.lst to screen *) lst := FileIO.StdOut;
-    END;
-
-    (* install error reporting procedure - Scanner.Error *)
+    BaseName(files[nFiles - 1], progName);
+    SymTab.Init;
+    QbeGen.OpenModule(progName);
     Error := StoreError;
 
-    (* instigate the compilation - Parser.Parse *)
-    FileIO.WriteString(FileIO.StdOut, "Parsing"); FileIO.WriteLn(FileIO.StdOut);
-    Parse;
+    (* parse each unit into the shared session *)
+    bad := FALSE; f := 0;
+    WHILE f < nFiles DO
+      Strings.Assign(files[f], sourceName);
+      FileIO.Open(src, sourceName, FALSE);
+      IF ~ FileIO.Okay THEN
+        FileIO.WriteString(FileIO.StdOut, "Could not open input file");
+        FileIO.WriteLn(FileIO.StdOut);
+        HALT
+      END;
+      ChangeExtension(sourceName, ".LST", listName);
+      FileIO.Open(lst, listName, TRUE);
+      IF ~ FileIO.Okay THEN lst := FileIO.StdOut END;
+      ResetErrors;
 
-    (* generate the source listing on lst file *)
-    PrintListing;
-    IF lst # FileIO.StdOut THEN FileIO.Close(lst) END;
-    
-    (* examine the outcome *)
-    IF NOT Successful()
-      THEN
+      FileIO.WriteString(FileIO.StdOut, "Parsing");
+      FileIO.WriteLn(FileIO.StdOut);
+      Parse;
+      PrintListing;
+      IF lst # FileIO.StdOut THEN FileIO.Close(lst) END;
+      IF NOT Successful() THEN
         PrintConsoleErrors(sourceName);
-        FileIO.WriteString(FileIO.StdOut, "Incorrect source");
-      ELSE
-        FileIO.WriteString(FileIO.StdOut, "Parsed correctly");
-        (* ++++++++ Add further activities if required ++++++++++ *)
+        bad := TRUE
+      END;
+      INC(f)
     END;
+
+    IF bad OR NOT SymTab.HaveProgram()
+      THEN FileIO.WriteString(FileIO.StdOut, "Incorrect source")
+      ELSE FileIO.WriteString(FileIO.StdOut, "Parsed correctly")
+    END
   END -->Grammar.

+ 6 - 0
compiler/tests/d_badimp_prog.mod

@@ -0,0 +1,6 @@
+MODULE DBadImpProg;
+FROM DLib IMPORT Nope;
+VAR ExitCode : INTEGER;
+BEGIN
+  ExitCode := 0
+END DBadImpProg.

+ 7 - 0
compiler/tests/d_basic.def

@@ -0,0 +1,7 @@
+DEFINITION MODULE DLib;
+(* Separate-compilation library: definition. *)
+CONST Base = 10;
+VAR counter : INTEGER;
+PROCEDURE Add(a, b : INTEGER) : INTEGER;
+PROCEDURE Bump;
+END DLib.

+ 15 - 0
compiler/tests/d_basic.mod

@@ -0,0 +1,15 @@
+IMPLEMENTATION MODULE DLib;
+(* Library implementation; BEGIN body initializes counter. *)
+PROCEDURE Add(a, b : INTEGER) : INTEGER;
+BEGIN
+  RETURN a + b
+END Add;
+
+PROCEDURE Bump;
+BEGIN
+  counter := counter + 1
+END Bump;
+
+BEGIN
+  counter := 5
+END DLib.

+ 9 - 0
compiler/tests/d_basic_prog.mod

@@ -0,0 +1,9 @@
+MODULE DBasicProg;
+FROM DLib IMPORT Base, Add, Bump, counter;
+VAR ExitCode : INTEGER;
+BEGIN
+  Bump;
+  Bump;
+  counter := counter + Add(Base, 32);
+  ExitCode := counter
+END DBasicProg.

+ 7 - 0
compiler/tests/d_opaque.def

@@ -0,0 +1,7 @@
+DEFINITION MODULE DOpaque;
+(* Opaque type: hidden behind a pointer for clients. *)
+TYPE Node;
+TYPE NodePtr = POINTER TO Node;
+PROCEDURE Init : NodePtr;
+PROCEDURE Value(p : NodePtr) : INTEGER;
+END DOpaque.

+ 17 - 0
compiler/tests/d_opaque.mod

@@ -0,0 +1,17 @@
+IMPLEMENTATION MODULE DOpaque;
+(* Completes opaque Node here; clients never see the record. *)
+TYPE Node = RECORD v : INTEGER END;
+VAR head : NodePtr;
+PROCEDURE Init : NodePtr;
+BEGIN
+  NEW(head);
+  head^.v := 55;
+  RETURN head
+END Init;
+
+PROCEDURE Value(p : NodePtr) : INTEGER;
+BEGIN
+  RETURN p^.v
+END Value;
+
+END DOpaque.

+ 8 - 0
compiler/tests/d_opaque_prog.mod

@@ -0,0 +1,8 @@
+MODULE DOpaqueProg;
+FROM DOpaque IMPORT NodePtr, Init, Value;
+VAR ExitCode : INTEGER;
+VAR p : NodePtr;
+BEGIN
+  p := Init;
+  ExitCode := Value(p)
+END DOpaqueProg.

+ 9 - 0
compiler/tests/d_qual_prog.mod

@@ -0,0 +1,9 @@
+MODULE DQualProg;
+(* Qualified cross-module access: IMPORT L and L.x / L.P. *)
+IMPORT DLib;
+VAR ExitCode : INTEGER;
+BEGIN
+  DLib.Bump;
+  DLib.counter := DLib.counter + DLib.Add(DLib.Base, 30);
+  ExitCode := DLib.counter
+END DQualProg.

+ 6 - 0
compiler/tests/d_types.def

@@ -0,0 +1,6 @@
+DEFINITION MODULE DTypes;
+TYPE Vec = ARRAY [0..2] OF INTEGER;
+TYPE Point = RECORD x, y : INTEGER END;
+PROCEDURE Sum(v : Vec) : INTEGER;
+PROCEDURE OriginX(p : Point) : INTEGER;
+END DTypes.

+ 15 - 0
compiler/tests/d_types.mod

@@ -0,0 +1,15 @@
+IMPLEMENTATION MODULE DTypes;
+PROCEDURE Sum(v : Vec) : INTEGER;
+VAR i, s : INTEGER;
+BEGIN
+  s := 0;
+  FOR i := 0 TO 2 DO s := s + v[i] END;
+  RETURN s
+END Sum;
+
+PROCEDURE OriginX(p : Point) : INTEGER;
+BEGIN
+  RETURN p.x
+END OriginX;
+
+END DTypes.

+ 10 - 0
compiler/tests/d_types_prog.mod

@@ -0,0 +1,10 @@
+MODULE DTypesProg;
+FROM DTypes IMPORT Vec, Point, Sum, OriginX;
+VAR ExitCode : INTEGER;
+VAR v : Vec;
+VAR p : Point;
+BEGIN
+  v[0] := 1; v[1] := 2; v[2] := 3;
+  p.x := 30; p.y := 40;
+  ExitCode := Sum(v) + OriginX(p)
+END DTypesProg.

+ 4 - 2
docs/blaise-phases-map.md

@@ -10,7 +10,7 @@ rediscovered. Refresh after every major step.
 | Blaise | Status there | Us | Status here |
 |---|---|---|---|
 | 1 Bootstrap pipeline | ✅ | Step 1 (M2→QBE hello) | ✅ done (`v3-step1`) |
-| 2 Type system (classes, records, ARC, exceptions) | ✅ | Steps 2–4.2 (scalars ✅, composites ✅, calls/frames/recursion ✅, nested + display ✅); separate compilation = 4.3 | ✅ complete modulo deliberate outs |
+| 2 Type system (classes, records, ARC, exceptions) | ✅ | Steps 2–4.3 (scalars ✅, composites ✅, calls/frames/recursion ✅, nested + display ✅, separate compilation ✅) | ✅ complete modulo deliberate outs |
 | 3 Generics + interfaces | ✅ | Explicitly OUT (R10 blueprints rejected) | ❌ deliberate |
 | 4 OPDF debug info | ✅ | Deferred (listing only) | ❌ deferred |
 | 5 Self-hosting | ✅ | Step 8 (last) | ⚠️ same mechanism, later slot |
@@ -72,4 +72,6 @@ no step lands on IR inspection alone.
 - 2026-09-22: 4.1 call emission (79/79); Phase-2 row →
   complete modulo deliberate outs (ARC/exceptions/generics).
 - 2026-09-22: 4.2 nested procedures + static-link display (81/81).
-- Next refresh: after 4.3 (separate compilation) or any Blaise phase flip.
+- 2026-09-22: 4.3 separate compilation, single-image session (87/87).
+  Step 4 complete; Phase-2 row closed.
+- Next refresh: start of step 5 (Benjamin stdlib).

+ 70 - 0
docs/summary_step4.3.md

@@ -0,0 +1,70 @@
+# V3 step 4.3 — separate compilation (done 2026-09-22)
+
+Single-image session model (V1/V2 lineage): one compiler run takes
+`lib.def lib.mod prog.mod ...` and produces one `<Prog>.ssa` image
+with the definitions, implementations and the program linked
+together. Suite 87/87 (44 run — incl. 4 trap-aborts + 4 sessions —
++ 43 reject). LL(1)-clean, zero gm2 warnings.
+
+## Session model
+
+- **Driver** (`compiler.frm`): collects all `NextParameter` files,
+  calls `SymTab.Init` and `QbeGen.OpenModule` once, then parses each
+  file into the shared table. The program module is last; a session
+  without one is `Incorrect source` (nothing to link).
+- **DEFINITION MODULE** — opens its own scope, records CONST/TYPE/VAR
+  and procedure headings (bodies discarded via `AbortFunc`), emits
+  nothing.
+- **IMPLEMENTATION MODULE** — resumes the definition's scope, lowers
+  procedure bodies to `export function`s, and its `BEGIN` body to
+  `<mod>_init`, which `main` calls at startup (before the program
+  body).
+- **Program MODULE** — imports resolved, emits `main`; the image is
+  named after its module (`EndModule(m1)`).
+
+## Modules / imports (SymTab)
+
+- Module registry (`modNames/modScopes/modKind/modImpl`): module
+  names live in the global scope as `KindModule`; each module gets a
+  child scope that *is* its export table.
+- `Materialize` clones an export into the current scope; shared by
+  `FROM L IMPORT x` and by qualified `L.x`/`L.P` (flatten-on-first-
+  use, idempotent). Unknown modules stay silent stubs (legacy);
+  a known module's missing export is 201.
+- `ResumeProc` lets an implementation heading re-open a definition's
+  procedure (fresh body scope, formals re-entered); its uid — hence
+  the mangled `$P_uid` symbol — matches what clients call.
+- **Opaque completion**: `OpaqueBase` detects a definition's opaque
+  alias (target still `InvalidType`); the implementation's
+  `TYPE T = ...` completes it via `SetTarget` instead of
+  re-declaring. `TYPE T;` stays usable behind `POINTER TO T`.
+
+## Backend (QbeGen)
+
+- Output is now fully buffered (`sessBuf`, 1 MB) and written at
+  `EndModule` under the program module's name, so a session whose
+  output name is only known once the program parses still lands
+  correctly. Nested-function hoisting keeps its per-depth buffers.
+- `BeginInit/EndInit` emit module init functions; `BeginBody` calls
+  each recorded init right after `main`'s `@start` and real-const
+  flush. `LoadDesignator` is the shared value-load path for
+  qualified access. Bare zero-arg function calls (`F` without
+  parentheses) now work.
+
+## Tests
+
+Sessions (`expect_run_files`, `expect_fail_files`):
+`DBasicProg` 49 (FROM imports of const/var/proc, impl `BEGIN`
+init), `DQualProg` 46 (qualified `L.x`/`L.P`, bare `DLib.Bump`),
+`DTypesProg` 36 (array + record types shared across units),
+`DOpaqueProg` 55 (opaque `Node` completed in the implementation,
+factory + accessor), `d_badimp_prog` 201 (missing export). The old
+`t_defunit`/`t_implunit`/`t_opaqueptr` "not supported yet" rejects
+became session-level rejections (definition-only / no program).
+
+## Deferred
+
+Strict signature checking (231) for impl-vs-def headings (arity is
+checked; types are not), variant records, per-file object emission
+(this is whole-program single-image), import cycles, `HIGH`/`LEN`,
+class lowering.

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott