Explorar o código

library: ISO ChanConsts/StreamFile; fix cross-unit set CONSTs

Library holes: add stdlib/chanconsts and stdlib/streamfile (ISO 10514-1)
layered on IOChan.  Regression t_streamfile (open/write/read/close + flag
handling, exit 42); the docs/wip drafts are superseded and removed.

Building StreamFile surfaced two real compiler bugs, both fixed:
- a set CONST exported from a definition module was pooled as $setN while
  a client's qualified reference emitted <unit>_<name> -> undefined
  symbol.  Module-level set consts are now emitted under their global
  name (QbeGen.PoolSetNamed; Lower uses SymTab.GlobalRef).
- Lower.EvalOrdConst did not fold a qualified enum literal (M.lit) inside
  a set constant -> spurious 230; it now resolves via
  SymTab.MaterializeAlias + GetSymVal.

Gates: suite 248/248, fixpoint OK (3,882,337 bytes), corpus 427/612
(unchanged), 0 crashes.
Eric Streit hai 3 días
pai
achega
c7d5cff707

+ 7 - 0
compiler/run_tests.sh

@@ -326,6 +326,13 @@ expect_run_files GTwiceProg 42 \
   g_twice.def g_twice.mod g_twice_i.def g_twice_i.mod \
   g_twice_c.def g_twice_c.mod g_twice_p.mod
 expect_run_files TByteBuf 62 t_bytebuf.mod
+expect_run_files TStreamFile 42 \
+  ../runtime/syslib/SysShim.def ../runtime/syslib/SysShim.mod \
+  ../runtime/syslib/FileIO.def ../runtime/syslib/FileIO.mod \
+  ../stdlib/iochan.def ../stdlib/iochan.mod \
+  ../stdlib/chanconsts.def ../stdlib/chanconsts.mod \
+  ../stdlib/streamfile.def ../stdlib/streamfile.mod \
+  t_streamfile.mod
 expect_run_files WrapProg 42 \
   ../runtime/syslib/SysShim.def ../runtime/syslib/SysShim.mod \
   ../stdlib/iochan.def ../stdlib/iochan.mod \

+ 30 - 12
compiler/src/Lower.mod

@@ -612,7 +612,7 @@ PROCEDURE MaskGetBit (VAR mask: ARRAY OF CARDINAL; bit: INTEGER): INTEGER;
 PROCEDURE EvalOrdConst (n: AST.Node; VAR v: INTEGER): BOOLEAN;
 (* The integer value of a compile-time ordinal expression, matching the
    grammar's folding (literals, named CONSTs, unary +/-, + - * DIV MOD). *)
-  VAR k: INTEGER; s, cv: QbeGen.QVal; nm: SymTab.Name;
+  VAR k: INTEGER; s, cv: QbeGen.QVal; nm, qnm, al: SymTab.Name;
       a, b: INTEGER; isCh: BOOLEAN;
   BEGIN
     IF n = AST.NoNode THEN RETURN FALSE END;
@@ -631,14 +631,26 @@ PROCEDURE EvalOrdConst (n: AST.Node; VAR v: INTEGER): BOOLEAN;
       IF SymTab.StrLen(s) = 3 THEN v := QbeGen.CharVal(s); RETURN TRUE END;
       RETURN FALSE
     ELSIF k = AST.NkDesignator THEN
-      IF (AST.NChild(n) # 1) OR (AST.Kind(AST.Child(n, 0)) # AST.NkIdent) THEN
-        RETURN FALSE END;
-      Name(AST.Child(n, 0), nm);
-      IF SymTab.Equal(nm, "TRUE") THEN v := 1; RETURN TRUE END;
-      IF SymTab.Equal(nm, "FALSE") THEN v := 0; RETURN TRUE END;
-      IF SymTab.Lookup(nm) AND (SymTab.SymKind(nm) = SymTab.KindConst)
-         AND SymTab.GetSymVal(nm, cv) AND QbeGen.IsImm(cv) THEN
-        RETURN SymTab.ConstInt(cv, v)
+      IF (AST.NChild(n) = 1) AND (AST.Kind(AST.Child(n, 0)) = AST.NkIdent) THEN
+        Name(AST.Child(n, 0), nm);
+        IF SymTab.Equal(nm, "TRUE") THEN v := 1; RETURN TRUE END;
+        IF SymTab.Equal(nm, "FALSE") THEN v := 0; RETURN TRUE END;
+        IF SymTab.Lookup(nm) AND (SymTab.SymKind(nm) = SymTab.KindConst)
+           AND SymTab.GetSymVal(nm, cv) AND QbeGen.IsImm(cv) THEN
+          RETURN SymTab.ConstInt(cv, v)
+        END;
+        RETURN FALSE
+      ELSIF (AST.NChild(n) = 2) AND (AST.Kind(AST.Child(n, 0)) = AST.NkIdent)
+         AND (AST.Kind(AST.Child(n, 1)) = AST.NkSelector)
+         AND (AST.Op(AST.Child(n, 1)) = AST.SelField) THEN
+        (* M.lit: a module-qualified constant (enum literal / CONST) *)
+        Name(AST.Child(n, 0), nm);
+        Name(AST.Child(AST.Child(n, 1), 0), qnm);
+        IF SymTab.MaterializeAlias(nm, qnm, al)
+           AND SymTab.GetSymVal(al, cv) AND QbeGen.IsImm(cv) THEN
+          RETURN SymTab.ConstInt(cv, v)
+        END;
+        RETURN FALSE
       END;
       RETURN FALSE
     ELSIF k = AST.NkUnary THEN
@@ -2518,7 +2530,7 @@ PROCEDURE EmitCountInits;
   END EmitCountInits;
 
 PROCEDURE LowerDecls (seq: AST.Node);
-  VAR i, j: CARDINAL; c: AST.Node; nm, txt: SymTab.Name; qv: QbeGen.QVal;
+  VAR i, j: CARDINAL; c: AST.Node; nm, txt, gn: SymTab.Name; qv: QbeGen.QVal;
       mg: QbeGen.QVal; uid, depth, np: CARDINAL; res, pt: SymTab.TypeIndex;
       ext, pv: BOOLEAN; kk: INTEGER; ct: SymTab.TypeIndex; savedProc: INTEGER;
       chunk, vn: AST.Node; pi: INTEGER;
@@ -2564,9 +2576,15 @@ PROCEDURE LowerDecls (seq: AST.Node);
         Name(AST.Child(c, 0), nm);
         kk := SymTab.ClassOf(AST.Ty(c));
         IF kk = SymTab.ClSet THEN
-          (* SET CONST: static data from the mask the grammar computed *)
+          (* SET CONST: static data from the mask the grammar computed.
+             A module-level const is emitted under its global name
+             (<unit>_<name>) so a client's qualified reference binds. *)
           IF LookupSetMask(nm, mk, mw, mlo) THEN
-            QbeGen.PoolSet(mk, mw, qv); SetConstVal(nm, qv)
+            IF SymTab.GlobalRef(nm, gn) THEN
+              QbeGen.PoolSetNamed(gn, mk, mw, qv)
+            ELSE QbeGen.PoolSet(mk, mw, qv)
+            END;
+            SetConstVal(nm, qv)
           ELSE SetConstVal(nm, "0")
           END
         ELSIF (kk = SymTab.ClArray) OR (kk = SymTab.ClRecord)

+ 4 - 0
compiler/src/QbeGen.def

@@ -370,6 +370,10 @@ PROCEDURE PoolSet (mask: ARRAY OF CARDINAL; words: CARDINAL; VAR q: QVal);
    `data $setN = { w mask0, ... }` and returns its address in q.  Lower
    calls this for set CONSTs (a set literal or a folded set expression
    whose mask the grammar computed). *)
+PROCEDURE PoolSetNamed (name: ARRAY OF CHAR; mask: ARRAY OF CARDINAL;
+                        words: CARDINAL; VAR q: QVal);
+(* Like PoolSet, but emits `data $<name>` (a module-level set CONST's
+   global name, what a client's qualified reference resolves to). *)
 
 PROCEDURE NewSetTemp (words: CARDINAL; VAR q: QVal);
 (* Fresh alloc8 stack slot for a set value; q is its address. *)

+ 20 - 0
compiler/src/QbeGen.mod

@@ -2407,6 +2407,26 @@ PROCEDURE PoolSet (mask: ARRAY OF CARDINAL; words: CARDINAL; VAR q: QVal);
     INC(setN)
   END PoolSet;
 
+PROCEDURE PoolSetNamed (name: ARRAY OF CHAR; mask: ARRAY OF CARDINAL;
+                        words: CARDINAL; VAR q: QVal);
+(* Like PoolSet but registers the descriptor as `data $<name>` — used for
+   a module-level set CONST, whose global name (<unit>_<const>) is what a
+   client's qualified reference emits. *)
+  VAR k: CARDINAL;
+  BEGIN
+    IF NOT opened OR noEmit THEN Cpy(q, "0"); RETURN END;
+    IF setN > HIGH(setW) THEN Cpy(q, "0"); RETURN END;
+    k := 0;
+    WHILE (k < words) AND (k <= HIGH(mask)) AND (k <= 15) DO
+      setMask[setN][k] := mask[k]; INC(k)
+    END;
+    WHILE k <= 15 DO setMask[setN][k] := 0; INC(k) END;
+    setW[setN] := words;
+    Cpy(setNam[setN], name);
+    Cpy(q, "$"); App(q, name);
+    INC(setN)
+  END PoolSetNamed;
+
 PROCEDURE NewSetTemp (words: CARDINAL; VAR q: QVal);
   VAR nb: QVal;
   BEGIN

+ 20 - 0
compiler/tests/t_streamfile.mod

@@ -0,0 +1,20 @@
+MODULE TStreamFile;
+IMPORT StreamFile, ChanConsts, IOChan;
+VAR cid : StreamFile.ChanId;
+    res : StreamFile.OpenResults;
+    ok  : INTEGER;
+    ExitCode : INTEGER;
+BEGIN
+  ok := 0;
+  StreamFile.Open(cid, "TStreamFile_tmp.tmp", StreamFile.write, res);
+  IF (res = ChanConsts.opened) AND StreamFile.IsStreamFile(cid) THEN INC(ok) END;
+  IOChan.WriteString(cid, "hello"); IOChan.WriteLn(cid);
+  StreamFile.Close(cid);
+  StreamFile.Open(cid, "TStreamFile_tmp.tmp", StreamFile.read, res);
+  IF res = ChanConsts.opened THEN INC(ok) END;
+  IF IOChan.ReadChar(cid) = "h" THEN INC(ok) END;
+  StreamFile.Close(cid);
+  StreamFile.Open(cid, "TStreamFile_tmp.tmp", StreamFile.raw, res);
+  IF res = ChanConsts.wrongFlags THEN INC(ok) END;
+  ExitCode := ok * 10 + 2
+END TStreamFile.

+ 10 - 4
docs/STATUS.md

@@ -4,9 +4,9 @@ This answers "where are we now?".
 
 ## Numbers
 
-- Suite **247/247** (`compiler/run_tests.sh`).
+- Suite **248/248** (`compiler/run_tests.sh`).
 - Self-hosting **FIXPOINT OK** — stage2 and stage3 images are
-  byte-identical (3,873,552 bytes).
+  byte-identical (3,882,337 bytes).
 - Corpus **427 / 612** sources compile with 0 errors, **0 crashes**
   (`tools/v3-corpus/corpus.sh` over the gm2 testsuite); the rest are
   genuine language gaps, **gm2 leniencies V3 deliberately rejects**, or
@@ -133,7 +133,13 @@ declarations still work (`t_fwdproc`, `t_fwdok`).
    `t_manyproc`), the label/`GOTO` tables, the 32-actual AST buffer and
    `SymTab`'s `MaxTypes` all report `230` (implementation limit) instead
    of truncating silently.  Still weak: error recovery and message polish.
-4. **Library holes** — channel/device framework (`docs/wip/`).
+4. **Library holes** — the channel framework grew `ChanConsts` and
+   `StreamFile` (ISO 10514-1) on top of `IOChan` (2026-10-19).  Still
+   missing: the remaining device/IO framework (`docs/wip/`).  Fixing
+   `StreamFile` also fixed a real bug: a **set CONST exported from a
+   definition module** is now emitted under its global name
+   (`<unit>_<name>`), so a client's qualified reference binds instead of
+   hitting an undefined symbol.
 5. **Housekeeping** — `docs/INDEX.md` is the entry point; the
    unreferenced historical records are under `docs/archive/` (2026-10-19).
    `resolve-check` was merged to `master`.
@@ -143,7 +149,7 @@ declarations still work (`t_fwdproc`, `t_fwdok`).
 ```sh
 cd compiler
 ./build.sh          # M2.atg -> scanner/parser/driver -> ./M2
-./run_tests.sh      # 247/247
+./run_tests.sh      # 248/248
 cd ..
 ./bootstrap/fixpoint.sh   # FIXPOINT OK (byte-identical stage2/stage3)
 ```

+ 19 - 3
docs/handoff-2026-10-19-next.md

@@ -2,7 +2,7 @@
 
 **Branch state (2026-10-19):** work is on `master`, tree clean.
 
-**Baseline:** suite **247/247**, fixpoint **OK (3,873,552 bytes)**,
+**Baseline:** suite **248/248**, fixpoint **OK (3,882,337 bytes)**,
 corpus **427/612, 0 crashes**.
 
 **Policy:** follow **Modula-2**, not gm2 degradation — declaration
@@ -105,7 +105,23 @@ the two-phase span table (silently dropped a body's codegen — regression
 `SymTab.MaxTypes` (`TypesOverflow`).  Remaining: error recovery and
 message polish, and the library holes (`docs/wip/`).
 
-**Baseline:** suite **247/247**, fixpoint **OK (3,873,552 bytes)**,
+## Library holes  ✅ first slice (2026-10-19)
+
+- `stdlib/chanconsts` + `stdlib/streamfile` (ISO 10514-1), layered on
+  `IOChan`.  Regression `t_streamfile` (open/write/read/close + flag
+  handling, exit 42).
+- This surfaced and fixed a real compiler bug: a **set CONST exported
+  from a definition module** was pooled as `$setN` while clients
+  referenced `<unit>_<name>` → undefined symbol.  Module-level set
+  consts are now emitted under their global name (`QbeGen.PoolSetNamed`;
+  `Lower` uses `SymTab.GlobalRef`), verified cross-unit by `t_streamfile`
+  (`StreamFile.write`) and a `ChanConsts` link test.  Also fixed
+  `Lower.EvalOrdConst` to fold a **qualified enum literal** (`M.lit`) in
+  set constants (a *different* blocker for the same file).
+- Remaining library work: the rest of the device/IO framework
+  (`docs/wip/`).
+
+**Baseline:** suite **248/248**, fixpoint **OK (3,882,337 bytes)**,
 corpus **427/612, 0 crashes**.
 
 ## P5 — housekeeping  ✅ DONE (2026-10-19)
@@ -142,7 +158,7 @@ commit with a `v3-*` tag.
 ```sh
 cd .../m2compiler-V3
 git checkout master
-cd compiler && ./build.sh && ./run_tests.sh   # 247/247
+cd compiler && ./build.sh && ./run_tests.sh   # 248/248
 cd .. && TMP="$PWD/.scratch/fixpoint" ./bootstrap/fixpoint.sh
 tools/v3-corpus/corpus.sh                     # 427 OK, 0 crash
 ```

+ 0 - 3
docs/wip/chanconsts.mod

@@ -1,3 +0,0 @@
-IMPLEMENTATION MODULE ChanConsts;
-(* Constants and types only. *)
-END ChanConsts.

+ 0 - 0
docs/wip/chanconsts.def → stdlib/chanconsts.def


+ 3 - 0
stdlib/chanconsts.mod

@@ -0,0 +1,3 @@
+IMPLEMENTATION MODULE ChanConsts;
+(* Flags and result codes only; nothing to implement. *)
+END ChanConsts.

+ 0 - 0
docs/wip/streamfile.def → stdlib/streamfile.def


+ 12 - 15
docs/wip/streamfile.mod → stdlib/streamfile.mod

@@ -1,6 +1,6 @@
 IMPLEMENTATION MODULE StreamFile;
-(* Open/close maps onto IOChan's OpenRead/OpenWrite; IsStreamFile is
-   TRUE for any open channel (V3's channels are all streams). *)
+(* Open/Close map onto IOChan's OpenRead/OpenWrite.  A channel is a
+   stream channel whenever it is open (V3's channels are all streams). *)
 
 IMPORT IOChan, ChanConsts;
 FROM ChanConsts IMPORT opened, noSuchFile, wrongFlags;
@@ -12,36 +12,33 @@ BEGIN
   RETURN f IN flags
 END HasFlag;
 
-PROCEDURE Open (VAR cid : IOChan.ChanId; name : ARRAY OF CHAR;
-                flags : ChanConsts.FlagSet;
-                VAR res : ChanConsts.OpenResults);
-  VAR c : IOChan.ChanId;
+PROCEDURE Open (VAR cid : ChanId; name : ARRAY OF CHAR;
+                flags : FlagSet; VAR res : OpenResults);
+  VAR c : ChanId;
 BEGIN
-  c := IOChan.MakeChan(NIL);
   IF HasFlag(flags, readFlag) THEN
-    (* read implies old *)
-    IF NOT IOChan.IsOpen(IOChan.OpenRead(name)) THEN
-      cid := c; res := noSuchFile; RETURN
-    END;
-    c := IOChan.OpenRead(name)
+    c := IOChan.OpenRead(name);
+    IF NOT IOChan.IsOpen(c) THEN
+      cid := IOChan.MakeChan(NIL); res := noSuchFile; RETURN
+    END
   ELSIF HasFlag(flags, writeFlag) THEN
     c := IOChan.OpenWrite(name);
     IF NOT IOChan.IsOpen(c) THEN
       cid := IOChan.MakeChan(NIL); res := noSuchFile; RETURN
     END
   ELSE
-    cid := c; res := wrongFlags; RETURN
+    cid := IOChan.MakeChan(NIL); res := wrongFlags; RETURN
   END;
   cid := c;
   res := opened
 END Open;
 
-PROCEDURE IsStreamFile (cid : IOChan.ChanId) : BOOLEAN;
+PROCEDURE IsStreamFile (cid : ChanId) : BOOLEAN;
 BEGIN
   RETURN IOChan.IsOpen(cid)
 END IsStreamFile;
 
-PROCEDURE Close (VAR cid : IOChan.ChanId);
+PROCEDURE Close (VAR cid : ChanId);
 BEGIN
   IOChan.Close(cid)
 END Close;