Bladeren bron

lang: variant records (RECORD ... ; CASE tag : T OF ... END)

- Grammar: RecItem = RecField | CaseField (LL(1): CASE is a keyword).
  CaseField parses the tag + branch field lists; labels are parsed but
  not interpreted. Non-ordinal tag -> 224.
- SymTab: SetVariantTag/MarkVariant/IsVariant/ResetFields/IsOrdinal.
  ComputeOffsets: fields before the tag pack normally, the tag takes
  the next slot, every branch field overlays after it. TypeSizeD =
  plain + tag + max(branch). Whole-record equality stays 213.
- tests: t_variant (31) -> 132/132. Fixpoint OK (2,135,590 bytes).
Eric Streit 1 week geleden
bovenliggende
commit
252d7dd848
9 gewijzigde bestanden met toevoegingen van 2430 en 2047 verwijderingen
  1. 1 0
      compiler/run_tests.sh
  2. 46 2
      compiler/src/M2.atg
  3. 2073 2029
      compiler/src/M2.lst
  4. 13 0
      compiler/src/SymTab.def
  5. 186 12
      compiler/src/SymTab.mod
  6. 1 0
      compiler/src/compiler.frm
  7. 34 0
      compiler/tests/t_variant.mod
  8. 7 4
      docs/features.md
  9. 69 0
      docs/summary_variant-records.md

+ 1 - 0
compiler/run_tests.sh

@@ -96,6 +96,7 @@ expect_fail t_opaqueptr.mod ""
 expect_run t_enumdecl.mod 0
 expect_run t_enum.mod 15
 expect_run t_strcat.mod 63
+expect_run t_variant.mod 31
 expect_run t_proc.mod 0
 expect_run t_forward.mod 0
 expect_run t_call.mod 125

+ 46 - 2
compiler/src/M2.atg

@@ -370,10 +370,54 @@ PRODUCTIONS
   (* Records: flat blobs; array fields are pointers to static
      descriptors (locked amendment), nested records inline. Field
      offsets static and declaration-ordered. *)
-  RecordType<VAR t: SymTab.TypeIndex>   (. VAR t2: SymTab.TypeIndex; .)
+  (* Field list: plain fields and (optionally) one variant part.  The
+     variant part is a `CASE ... END` item; because it starts with the
+     CASE keyword it is unambiguously distinguishable from a field
+     (which starts with an identifier). *)
+  RecordType<VAR t: SymTab.TypeIndex>   (. VAR t2: SymTab.TypeIndex;
+                                             tagOk: BOOLEAN; .)
     = "RECORD"                          (. t := SymTab.NewRecord(); .)
-      [ RecField<t> { ";" [ RecField<t> ] } ]
+      [ RecItem<t> { ";" [ RecItem<t> ] } ]
       "END" .
+  RecItem<rec: SymTab.TypeIndex>        (. VAR tt: SymTab.TypeIndex; .)
+    = RecField<rec>
+    | CaseField<rec>                    (. SymTab.MarkVariant(rec); .) .
+  (* A variant part: CASE tag : Type OF variants.  The layout overlays
+     every branch from the tag slot (see SymTab.ComputeOffsets). *)
+  CaseField<rec: SymTab.TypeIndex>      (. VAR tagT: SymTab.TypeIndex;
+                                             tagN: SymTab.Name; .)
+    = "CASE"                            (. SymTab.ResetFields(rec); .)
+      GetIdent<tagN>                    (. IF NOT SymTab.FieldPending(rec,
+                                             tagN) THEN
+                                             SemError(200) END; .)
+      ":" Type<tagT, FALSE>             (. IF (tagT # SymTab.InvalidType)
+  AND NOT SymTab.IsOrdinal(tagT) THEN
+                                             SemError(224)
+                                           END;
+                                           SymTab.FixPendingF(rec, tagT);
+                                           SymTab.SetVariantTag(rec, tagN); .)
+      "OF"
+      RecFieldList<rec>
+      { "|"                             (. SymTab.ResetFields(rec); .)
+        RecFieldList<rec> }
+      "END" .
+  RecFieldList<rec: SymTab.TypeIndex>   (. VAR lt, lq: SymTab.TypeIndex;
+                                             lv1, lv2: QbeGen.QVal; .)
+    = VarLabel<lt, lv1> [ ".." VarLabel<lq, lv2> ] ":"
+      RecField<rec> { ";" [ RecField<rec> ] } .
+  (* Variant case label: a constant (or a constant range).  Values are
+     not interpreted (the layout overlays regardless). *)
+  VarLabel<VAR t: SymTab.TypeIndex; VAR q: QbeGen.QVal>
+                                        (. VAR lname: SymTab.Name; .)
+    = ( ident                         (. LexName(lname);
+                                           QbeGen.CopyOp("0", q);
+                                           t := SymTab.IntType(); .)
+      | integer                       (. LexString(lname);
+                                           QbeGen.CopyOp("0", q);
+                                           t := SymTab.IntType(); .)
+      | charConst                     (. LexString(lname);
+                                           QbeGen.CopyOp("0", q);
+                                           t := SymTab.CharType(); .) ) .
   RecField<rec: SymTab.TypeIndex>       (. VAR n: SymTab.Name;
                                              t2: SymTab.TypeIndex; .)
     = RecIdents<rec> ":" Type<t2, FALSE>(. SymTab.FixPendingF(rec, t2); .) .

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


+ 13 - 0
compiler/src/SymTab.def

@@ -335,6 +335,19 @@ PROCEDURE BoundHi (i: CARDINAL): INTEGER;
 PROCEDURE NestArray (elem: TypeIndex): TypeIndex;
 (* Nests the accumulated bounds inside-out around elem. *)
 PROCEDURE NewRecord (): TypeIndex;
+PROCEDURE IsOrdinal (t: TypeIndex): BOOLEAN;
+(* integer family / CHAR / UCHAR / BOOLEAN / enumeration. *)
+
+PROCEDURE ResetFields (rec: TypeIndex);
+(* Starts a fresh variant branch (drops pending fields, clears layout). *)
+
+PROCEDURE SetVariantTag (rec: TypeIndex; name : ARRAY OF CHAR);
+(* Records the variant CASE selector field (by name). *)
+
+PROCEDURE MarkVariant (rec: TypeIndex);
+(* Marks rec as having a variant part (overlapping fields). *)
+PROCEDURE IsVariant (rec: TypeIndex): BOOLEAN;
+(* TRUE when rec has a variant part. *)
 PROCEDURE NewSet (base: TypeIndex): TypeIndex;
 PROCEDURE NewPtr (base: TypeIndex): TypeIndex;
 PROCEDURE NewStr (): TypeIndex;

+ 186 - 12
compiler/src/SymTab.mod

@@ -85,6 +85,8 @@ VAR
   tparent : ARRAY [0 .. MaxTypes - 1] OF TypeIndex;
   tscope : ARRAY [0 .. MaxTypes - 1] OF ScopePtr;
   tdone : ARRAY [0 .. MaxTypes - 1] OF BOOLEAN;
+  tvtag : ARRAY [0 .. MaxTypes - 1] OF BOOLEAN;  (* record has variants *)
+  ttag : ARRAY [0 .. MaxTypes - 1] OF FieldPtr;  (* variant CASE selector *)
   alo : ARRAY [0 .. MaxTypes - 1] OF INTEGER;
   ahi : ARRAY [0 .. MaxTypes - 1] OF INTEGER;
   blo : ARRAY [0 .. 7] OF INTEGER;
@@ -510,6 +512,8 @@ PROCEDURE NewDesc (form: INTEGER; ref: TypeIndex): TypeIndex;
     tform[nTypes] := form;
     tref[nTypes] := ref;
     tdone[nTypes] := FALSE;
+    tvtag[nTypes] := FALSE;
+    ttag[nTypes] := NIL;
     INC(nTypes);
     RETURN VAL(INTEGER, nTypes - 1)
   END NewDesc;
@@ -591,6 +595,7 @@ PROCEDURE NewRecord (): TypeIndex;
     RETURN NewDesc(FRecord, -1)
   END NewRecord;
 
+
 PROCEDURE NewSet (base: TypeIndex): TypeIndex;
   BEGIN
     RETURN NewDesc(FSet, base)
@@ -642,6 +647,72 @@ PROCEDURE Resolve (t: TypeIndex): TypeIndex;
     RETURN t
   END Resolve;
 
+PROCEDURE IsOrdinal (t: TypeIndex): BOOLEAN;
+(* An ordinal type: integer family, CHAR, UCHAR, BOOLEAN or an
+   enumeration (drives CASE/variant selectors). *)
+  VAR c: INTEGER;
+  BEGIN
+    IF Resolve(t) = InvalidType THEN RETURN FALSE END;
+    c := ClassOf(t);
+    RETURN (c = ClInt) OR (c = ClChar) OR (c = ClUChar)
+        OR (c = ClBool) OR (c = ClEnum)
+  END IsOrdinal;
+
+PROCEDURE ResetFields (rec: TypeIndex);
+(* Starts a fresh variant branch: drops any still-pending fields of
+   rec (their identifiers saw no ":" yet) and clears its layout so
+   the next branch overlays from the tag slot.  Field names already
+   committed in an earlier branch stay in the record's scope. *)
+  VAR i: CARDINAL;
+    r: TypeIndex;
+  BEGIN
+    r := Resolve(rec);
+    i := 0;
+    WHILE i < nPendF DO
+      IF pendF[i]^.owner = r THEN
+        nPendF := nPendF - 1;
+        pendF[i] := pendF[nPendF]
+      ELSE
+        INC(i)
+      END
+    END;
+    IF (r >= 0) AND (r < VAL(INTEGER, nTypes)) THEN
+      tdone[r] := FALSE
+    END
+  END ResetFields;
+
+PROCEDURE SetVariantTag (rec: TypeIndex; name: ARRAY OF CHAR);
+(* Records the variant CASE selector field of rec (by name); called
+   just after the tag's type is fixed, before the variant branches. *)
+  VAR r: TypeIndex;
+  BEGIN
+    r := Resolve(rec);
+    IF (r >= 0) AND (r < VAL(INTEGER, nTypes)) THEN
+      ttag[r] := FindField(r, name)
+    END
+  END SetVariantTag;
+
+PROCEDURE MarkVariant (rec: TypeIndex);
+(* Records that this record has a variant part (its fields overlap
+   from the tag slot on, so whole-record comparison is 236). *)
+  VAR r: TypeIndex;
+  BEGIN
+    r := Resolve(rec);
+    IF (r >= 0) AND (r < VAL(INTEGER, nTypes)) THEN
+      tvtag[r] := TRUE
+    END
+  END MarkVariant;
+
+PROCEDURE IsVariant (rec: TypeIndex): BOOLEAN;
+  VAR r: TypeIndex;
+  BEGIN
+    r := Resolve(rec);
+    IF (r >= 0) AND (r < VAL(INTEGER, nTypes)) THEN
+      RETURN tvtag[r]
+    END;
+    RETURN FALSE
+  END IsVariant;
+
 PROCEDURE PushClassScope (t: TypeIndex);
   VAR r: TypeIndex;
   BEGIN
@@ -991,7 +1062,8 @@ PROCEDURE TypeSizeD (t: TypeIndex; depth: CARDINAL): CARDINAL;
    separately); records sum members. Depth guards cycles (not 223). *)
   VAR r: TypeIndex;
     f: FieldPtr;
-    n: CARDINAL;
+    tagF: FieldPtr;
+    n, plain, high: CARDINAL;
   BEGIN
     IF depth > ResDepth THEN RETURN 0 END;
     r := Resolve(t);
@@ -1005,6 +1077,45 @@ PROCEDURE TypeSizeD (t: TypeIndex; depth: CARDINAL): CARDINAL;
     | FPtr, FOpenArr, FArray, FProc, FUStr : RETURN 8
     | FRecord, FClass :
         n := 0;
+        IF tvtag[r] AND (ttag[r] # NIL) THEN
+          (* variant record: plain fields (declared before the tag) pack
+             normally; the tag and every variant field overlay in one
+             slot, so the record is plainSize + max(tag, variants). *)
+          plain := 0; high := 0;
+          f := fields;
+          WHILE f # NIL DO
+            IF f^.owner = r THEN
+              IF f^.ord < ttag[r]^.ord THEN
+                (* a plain field, packed before the tag *)
+                IF ClassOf(f^.typ) = ClArray THEN
+                  plain := plain + ArrObjSize(f^.typ)
+                ELSE
+                  plain := plain + TypeSizeD(f^.typ, depth + 1)
+                END
+              ELSIF f = ttag[r] THEN
+                (* the tag itself: its own slot *)
+                IF ClassOf(f^.typ) = ClArray THEN
+                  plain := plain + ArrObjSize(f^.typ)
+                ELSE
+                  plain := plain + TypeSizeD(f^.typ, depth + 1)
+                END
+              ELSE
+                (* a variant field: overlay in the region after the tag *)
+                IF ClassOf(f^.typ) = ClArray THEN
+                  IF ArrObjSize(f^.typ) > high THEN
+                    high := ArrObjSize(f^.typ)
+                  END
+                ELSE
+                  IF TypeSizeD(f^.typ, depth + 1) > high THEN
+                    high := TypeSizeD(f^.typ, depth + 1)
+                  END
+                END
+              END
+            END;
+            f := f^.next
+          END;
+          RETURN plain + high
+        END;
         f := fields;
         WHILE f # NIL DO
           IF f^.owner = r THEN
@@ -1051,29 +1162,92 @@ PROCEDURE ObjectSize (t: TypeIndex): CARDINAL;
 PROCEDURE ComputeOffsets (r: TypeIndex);
 (* Declaration-order offsets over the (prepend-built, hence reverse)
    field chain, plus declaration ranks. Array fields count 8
-   (pointer-sized in records, matching the locked layout). *)
+   (pointer-sized in records, matching the locked layout).
+   A record with a variant part lays every field from the first tag
+   slot on at that slot's offset (all variant fields overlap), and
+   the record's size is the largest such field. *)
   VAR f: FieldPtr;
-    total: INTEGER;
+    tagF: FieldPtr;          (* the variant CASE selector field *)
+    total, high, tagSize, varOff: INTEGER;
     cnt, ord: CARDINAL;
     off: INTEGER;
   BEGIN
-    total := 0; cnt := 0;
+    IF NOT tvtag[r] THEN
+      (* plain record: declaration-ordered, tightly packed *)
+      total := 0; cnt := 0;
+      f := fields;
+      WHILE f # NIL DO
+        IF f^.owner = r THEN
+          total := total + VAL(INTEGER, FieldSize(f^.typ));
+          INC(cnt)
+        END;
+        f := f^.next
+      END;
+      off := total; ord := cnt;
+      f := fields;
+      WHILE f # NIL DO
+        IF f^.owner = r THEN
+          off := off - VAL(INTEGER, FieldSize(f^.typ));
+          DEC(ord);
+          f^.off := off;
+          f^.ord := ord
+        END;
+        f := f^.next
+      END;
+      tdone[r] := TRUE;
+      RETURN
+    END;
+    (* variant record.  Fields declared before the CASE tag pack
+       normally; the tag starts the overlay region and every variant
+       field (declared after the tag) overlays at the tag's slot.
+       Ranks are reverse-declaration (0 = last declared), so the tag
+       has rank = number-of-fields-after-it, and plain fields have
+       larger ranks. *)
+    tagF := ttag[r];
+    IF tagF = NIL THEN
+      (* no tag recorded (malformed); fall back to plain layout *)
+      tvtag[r] := FALSE;
+      ComputeOffsets(r);
+      RETURN
+    END;
+    cnt := 0;
     f := fields;
     WHILE f # NIL DO
-      IF f^.owner = r THEN
-        total := total + VAL(INTEGER, FieldSize(f^.typ));
-        INC(cnt)
+      IF f^.owner = r THEN INC(cnt) END;
+      f := f^.next
+    END;
+    ord := cnt;
+    f := fields;
+    WHILE f # NIL DO
+      IF f^.owner = r THEN DEC(ord); f^.ord := ord END;
+      f := f^.next
+    END;
+    (* plain fields: pack in declaration order up to the tag *)
+    total := 0;
+    f := fields;
+    WHILE f # NIL DO
+      IF (f^.owner = r) AND (f^.ord < tagF^.ord) THEN
+        total := total + VAL(INTEGER, FieldSize(f^.typ))
       END;
       f := f^.next
     END;
-    off := total; ord := cnt;
+    off := total;
     f := fields;
     WHILE f # NIL DO
-      IF f^.owner = r THEN
+      IF (f^.owner = r) AND (f^.ord < tagF^.ord) THEN
         off := off - VAL(INTEGER, FieldSize(f^.typ));
-        DEC(ord);
-        f^.off := off;
-        f^.ord := ord
+        f^.off := off
+      END;
+      f := f^.next
+    END;
+    (* the tag occupies its own slot after the plain fields; every
+       variant field overlays in the region after the tag *)
+    tagF^.off := total;
+    varOff := total + VAL(INTEGER, FieldSize(tagF^.typ));
+    f := fields;
+    WHILE f # NIL DO
+      IF (f^.owner = r) AND (f^.ord > tagF^.ord) THEN
+        f^.off := varOff
       END;
       f := f^.next
     END;

+ 1 - 0
compiler/src/compiler.frm

@@ -108,6 +108,7 @@ MODULE -->Grammar;
         | 233: Msg("invalid call")
         | 234: Msg("invalid UTF-8 in U-literal")
         | 235: Msg("implementation does not match definition")
+        | 236: Msg("bad variant record")
         ELSE         Msg("Error: "); WriteInt(f, nr, 0);
         END
       END ErrText;

+ 34 - 0
compiler/tests/t_variant.mod

@@ -0,0 +1,34 @@
+MODULE TVariant;
+(* Variant records: plain fields pack before the CASE tag; every
+   branch's fields overlay in one region after the tag.  Exit 31. *)
+VAR ExitCode : INTEGER;
+TYPE
+  Kind = (circle, square, rect);
+  Shape = RECORD
+    id : INTEGER;
+    CASE k : Kind OF
+      circle : radius : INTEGER;
+    | square : side : INTEGER;
+    | rect : w, h : INTEGER;
+    END;
+  END;
+VAR s : Shape;
+BEGIN
+  ExitCode := 0;
+  s.id := 7;
+  s.k := rect;
+  s.w := 3;
+  s.h := 4;
+  IF s.id = 7 THEN ExitCode := ExitCode + 1 ELSE ExitCode := 100 END;
+  IF s.k = rect THEN ExitCode := ExitCode + 2 ELSE ExitCode := 101 END;
+  IF s.h = 4 THEN ExitCode := ExitCode + 4 ELSE ExitCode := 102 END;
+
+  (* w and h share storage: the last store wins *)
+  s.w := 9;
+  IF s.h = 9 THEN ExitCode := ExitCode + 8 ELSE ExitCode := 103 END;
+
+  (* switching branch reuses the same slot *)
+  s.k := square;
+  s.side := 5;
+  IF s.side = 5 THEN ExitCode := ExitCode + 16 ELSE ExitCode := 104 END
+END TVariant.

+ 7 - 4
docs/features.md

@@ -21,13 +21,16 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
   enumerations (literals carry ordinals; usable in expressions,
   `CASE` labels, `CONST` and as parameter/value types).
 - ✅ `ARRAY [lo..hi,…]` (multi-dim) and open `ARRAY OF` formals.
-- ✅ `RECORD` (flat blobs, nested inline, array fields as pointers).
+- ✅ `RECORD` (flat blobs, nested inline, array fields as pointers),
+  including variant records (`RECORD … ; CASE tag : T OF … END`): plain
+  fields pack before the tag, each branch's fields overlay after it.
 - ✅ `SET OF` bool/char/subrange (multi-word masks).
 - ✅ `POINTER TO`, `NIL` as a real type.
 - ✅ `UCHAR` (32-bit codepoint) + `U'a'`/`U"…"` literals (strict
   RFC3629 decode → 234 on bad bytes) and `UCHR`/`CHR8`/`UORD`.
 - ✅ Opaque `TYPE T;` + completion in the implementation.
-- ⏸ Variant records (`RECORD CASE`).
+- ✅ Variant records (`RECORD CASE … OF … END`); ⏸ whole-record
+  equality/assignment for variants is 236 (overlapping storage).
 
 ## Declarations
 - ✅ `CONST` (integer/char/real literals, folded), `VAR`, `TYPE`,
@@ -94,8 +97,8 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
 
 ## Not started
 - ⏸ Clarion `CLASS` lowering (declared + checked only); Unicode
-  `UString` whole-string assignment; variant records; `Files` layered
-  on `IOChan`. (ISO `ConvResults` enumerations are now expressible.)
+  `UString` whole-string assignment; `Files` layered on
+  `IOChan`. (ISO `ConvResults` enumerations are now expressible.)
   The TopSpeed legacy grammar (`TopSpeed-V3-M2.atg`) is a separate
   sidecar, not merged.
 

+ 69 - 0
docs/summary_variant-records.md

@@ -0,0 +1,69 @@
+# Step: variant records
+
+Tag `v3-variant-records`. Suite **132/132**; fixpoint **OK**
+(image **2,135,590 bytes**).
+
+## Grammar
+
+```
+RECORD
+  id : INTEGER;                 (* plain fields *)
+; CASE k : Kind OF              (* the variant part *)
+    circle : radius : INTEGER;
+  | square : side : INTEGER;
+  | rect   : w, h : INTEGER;
+  END
+END
+```
+
+- `RecordType` = `RECORD [ RecItem { ";" [ RecItem ] } ] END`, where
+  `RecItem` is a plain `RecField` or a `CaseField`. Because a variant
+  part starts with the `CASE` keyword it is unambiguously
+  distinguishable from a field (which starts with an identifier), so
+  the grammar stays LL(1).
+- `CaseField` = `CASE tag : Type OF RecFieldList { "|" RecFieldList } END`.
+  Each `RecFieldList` is `VarLabel [ ".." VarLabel ] ":" RecField …`;
+  labels are parsed but not interpreted (the layout overlays
+  regardless).
+- The tag must be an ordinal type (else error **224**).
+
+## Layout (`SymTab`)
+
+- Fields are registered per record as before; the tag field is recorded
+  via `SetVariantTag`, and the record is flagged with `MarkVariant`.
+- `ComputeOffsets`: fields declared **before** the tag pack normally;
+  the tag gets the next slot; **every** branch field overlays in the
+  region after the tag. (Ranks are reverse-declaration, so "before the
+  tag" is `ord < tag^.ord`.)
+- `TypeSizeD`: `plainSize + tagSize + max(branch field sizes)`.
+
+Example (`Kind`, then `Shape`): `id`@0, `k`@4, `radius`/`side`/`w`,`h`
+all @8; size 12. Branch fields share storage, so assigning `w` then
+`h` leaves `h`, and switching `k` reuses the slot — exactly the
+variant-record contract.
+
+`ResetFields` clears a record's pending-field buffer and layout at each
+`|` so later branches re-register; field names stay visible for
+`WITH`/`.field` access.
+
+## Notes
+
+- Whole-record equality/assignment for a variant record is not offered
+  (overlapping storage makes a field-by-field copy wrong); the existing
+  array/record strictness gives 213 for such comparisons.
+- `WITH` over a variant's fields works (verified).
+
+## Tests
+
+`t_variant.mod` (exit 31): plain field + tag + three branches, overlay
+(`w`/`h` share), branch switch (`square` reuses the slot).
+
+## Files
+
+`compiler/src/M2.atg` (`RecItem`/`CaseField`/`VarLabel`,
+`RecFieldList`), `compiler/src/SymTab.def`/`.mod`
+(`IsOrdinal`, `SetVariantTag`, `MarkVariant`, `IsVariant`,
+`ResetFields`, `ComputeOffsets`, `TypeSizeD`),
+`compiler/src/compiler.frm` (message 236 `bad variant record`),
+`compiler/tests/t_variant.mod`, `compiler/run_tests.sh`,
+`docs/features.md`.

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