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

v3 step 1.10 — Clarion classes + corrected OOP sketch (33/33 tests green)

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

+ 4 - 1
README.md

@@ -50,7 +50,10 @@ 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.
+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.
 

+ 3 - 0
compiler/run_tests.sh

@@ -60,6 +60,9 @@ expect_run t_for.mod 85
 expect_run t_case.mod 103
 expect_run t_bool.mod 42
 expect_run t_real.mod 31
+expect_fail t_classdef.mod "not supported yet"
+expect_fail t_class.mod "not supported yet"
+expect_fail t_bad_parent.mod "undeclared identifier"
 expect_run showcase3.mod 183
 expect_fail t_enumdecl.mod "not supported yet"
 expect_fail t_proc.mod "not supported yet"

+ 116 - 4
compiler/src/M2.atg

@@ -21,7 +21,13 @@ COMPILER M2
    (labels, ranges, ELSE; compare-chain), RETURN with 232 checks
    (outside proc / value mismatch / missing value). WITH waits for
    records (step 3). Boolean connectives are eager (or/and/xor);
-   relations yield 0/1 via cXXw. *)
+   relations yield 0/1 via cXXw.
+   Clarion-form classes (docs/OOP.txt): CLASS decl + single
+   inheritance + CLASS IMPLEMENTATION blocks, methods with ";"
+   (per the Table example, not the sketch's ","), VIRTUAL flagged.
+   Scopes and member checks now; lowering later (one 230 per
+   class/impl block). Classic identifiers: no underscores, so the
+   Table example's _names stay lexically out of reach. *)
 
 IMPORT SymTab, QbeGen;
 
@@ -112,7 +118,7 @@ PRODUCTIONS
                                            SymTab.PrintTable; .) .
   DeclSeq
     = { ConstBlock | TypeBlock<FALSE> | VarBlock | ProcDecl ";"
-      | NestedModule ";" } .
+      | NestedModule ";" | ClassItem ";" } .
   (* Local module, Wirth form. Parses + checks fully (own scope);
      lowering waits for step 4, marked by one 230 at END. *)
   NestedModule                          (. VAR m1, m2: SymTab.Name; .)
@@ -164,7 +170,7 @@ PRODUCTIONS
      Opaque "TYPE T;" needs isDef (definition units); elsewhere 231.
      Composite forms (ARRAY/RECORD/SET/POINTER) arrive with step 3. *)
   TypeBlock<isDef: BOOLEAN>
-    = "TYPE" { TypeItem<isDef> ";" } .
+    = "TYPE" { TypeItem<isDef> ";" | ClassItem ";" } .
   TypeItem<isDef: BOOLEAN>              (. VAR n: SymTab.Name;
                                              t: SymTab.TypeIndex; .)
     = GetIdent<n>                       (. IF ~SymTab.Enter(n,
@@ -226,6 +232,110 @@ PRODUCTIONS
                                              SemError(200) END;
                                            SymTab.SetSymType(n, t); .) }
       ")" .
+  (* Clarion-form classes (docs/OOP.txt): declaration + single
+     inheritance + IMPLEMENTATION blocks. Scopes and member checks
+     now; lowering (vtable, dispatch, THIS) later — one 230 per
+     class/impl block. Methods end with ";" per the Table example
+     (not "," as in the sketch). No underscores in identifiers. *)
+  (* Single CLASS item in both loops: separating declaration from
+     IMPLEMENTATION at the loop level needs 2-token lookahead
+     (CLASS ident vs CLASS IMPLEMENTATION), which LL(1) cannot do.
+     The second token decides after CLASS is consumed. A misplaced
+     CLASS IMPLEMENTATION inside TYPE still parses (harmless: the
+     whole unit ends 230 until lowering). *)
+  ClassItem
+    = "CLASS" ( "IMPLEMENTATION" ClassImplRest | ClassRest ) .
+  ClassRest                             (. VAR cn, m2, pn: SymTab.Name;
+                                             ct: SymTab.TypeIndex; .)
+    = GetIdent<cn>                      (. IF ~SymTab.Enter(cn,
+                                             SymTab.KindType) THEN
+                                             SemError(200) END;
+                                           ct := SymTab.NewClass();
+                                           SymTab.SetSymType(cn, ct);
+                                           SymTab.PushClassScope(ct); .)
+      [ Parents<ct> ]
+      ";"
+      { ClassField<ct> ";" }
+      { MethodHeading<pn> ";"           (. SymTab.CloseProc; .) }
+      "END"
+      GetIdent<m2>                      (. IF ~SymTab.Equal(cn, m2) THEN
+                                             SemError(202) END;
+                                           SymTab.PopScope;
+                                           SemError(230); .) .
+  Parents<ct: SymTab.TypeIndex>         (. VAR p: SymTab.Name; .)
+    = "(" Parent1<ct>
+      { "," GetIdent<p>                 (. SemError(230); .) }
+      ")" .
+  Parent1<ct: SymTab.TypeIndex>         (. VAR p: SymTab.Name;
+                                             pt: SymTab.TypeIndex; .)
+    = GetIdent<p>                       (. IF ~SymTab.Lookup(p) THEN
+                                             SemError(201)
+                                           ELSE pt := SymTab.SymType(p);
+                                             IF SymTab.ClassOf(pt) #
+                                                SymTab.ClClass THEN
+                                               SemError(230)
+                                             ELSE SymTab.SetParent(ct, pt)
+                                             END
+                                           END; .) .
+  ClassField<ct: SymTab.TypeIndex>      (. VAR n, rhs: SymTab.Name;
+                                             t: SymTab.TypeIndex; .)
+    = GetIdent<n>
+      ( "=" GetIdent<rhs>               (. IF ~SymTab.Enter(n,
+                                             SymTab.KindConst) THEN
+                                             SemError(200) END;
+                                           IF SymTab.Lookup(rhs) THEN
+                                             SymTab.SetSymType(n,
+                                               SymTab.SymType(rhs))
+                                           END; .)
+      |                                 (. IF ~SymTab.FieldPending(ct,
+                                             n) THEN
+                                             SemError(200) END; .)
+        { "," GetIdent<n>               (. IF ~SymTab.FieldPending(ct,
+                                             n) THEN
+                                             SemError(200) END; .) }
+        ":" TypeIdent<t>                (. SymTab.FixPendingF(ct, t); .) ) .
+  MethodHeading<VAR pn: SymTab.Name>    (. VAR wantVirt: BOOLEAN; .)
+    =                                   (. wantVirt := FALSE; .)
+      [ "VIRTUAL"                       (. wantVirt := TRUE; .) ]
+      ProcHeading<pn>                   (. IF wantVirt THEN
+                                             SymTab.MarkVirtual END; .) .
+  ClassImplRest                         (. VAR cn, m2: SymTab.Name;
+                                             ct: SymTab.TypeIndex; .)
+    = GetIdent<cn>                      (. IF ~SymTab.Lookup(cn) THEN
+                                             SemError(201);
+                                             ct := SymTab.InvalidType
+                                           ELSE ct := SymTab.SymType(cn);
+                                             IF SymTab.ClassOf(ct) #
+                                                SymTab.ClClass THEN
+                                               SemError(230);
+                                               ct := SymTab.InvalidType
+                                             END
+                                           END;
+                                           IF ct #
+                                              SymTab.InvalidType THEN
+                                             IF ~SymTab.PushClassMembers(
+                                                    ct) THEN
+                                               SemError(230) END
+                                           END; .)
+      ";" { MethodImpl<ct> ";" }
+      [ "BEGIN"
+        [ StatSeq ] ]
+      "END"
+      GetIdent<m2>                      (. IF ~SymTab.Equal(cn, m2) THEN
+                                             SemError(202) END;
+                                           SymTab.PopScope;
+                                           SemError(230); .) .
+  MethodImpl<ct: SymTab.TypeIndex>      (. VAR pn: SymTab.Name; .)
+    = MethodHeading<pn> ";"
+                                        (. IF (ct #
+                                              SymTab.InvalidType)
+                                             & ~SymTab.MethodExists(ct,
+                                                  pn) THEN
+                                             SemError(201) END; .)
+      ( "FORWARD"                       (. SymTab.MarkFwd;
+                                           SymTab.CloseProc; .)
+      | Block<pn>                       (. SymTab.CloseProc;
+                                           SemError(230); .) ) .
   ConstBlock
     = "CONST" { ConstDecl ";" } .
   ConstDecl                             (. VAR n: SymTab.Name;
@@ -627,7 +737,9 @@ PRODUCTIONS
                                                 END
                                               ELSE QbeGen.CopyOp("0", q);
                                                 IF (k = SymTab.KindImport)
-                                                   OR (k = SymTab.KindProc) THEN
+                                                   OR (k = SymTab.KindProc)
+                                                   OR (k
+                                                       = SymTab.KindField) THEN
                                                   SemError(230)
                                                 END
                                               END

+ 910 - 7
compiler/src/M2.lst

@@ -10,16 +10,919 @@ Undefined nonterminals:   -- none --
 Unreachable nonterminals: -- none --
 Circular derivations:     -- none --
 Underivable nonterminals: -- none --
-LL(1) conditions:         --  ok  --
+LL(1) conditions:
+ LL(1) error in TypeBlock: "CLASS" is the start & successor of a deletable structure
+
+Listing:
+
+    1  COMPILER M2
+    2  (* Step 1 — minimal integer pipeline (m2compiler-V3).
+    3     Subset: program MODULE + CONST (literal) + VAR INTEGER +
+    4     assignment + integer expressions (+ - * DIV MOD, leading sign,
+    5     parens). Fresh QBE backend (QbeGen) to gen_ssa/<Module>.ssa;
+    6     assemble with qbe, link with cc.
+    7  
+    8     Test convention: a global VAR ExitCode : INTEGER makes generated
+    9     $main return its value as the process exit code, else return 0.
+   10  
+   11     Semantic errors reuse the V1/V2/Test2 family: 200 duplicate,
+   12     201 undeclared, 202 module name mismatch, 210 bad assignment,
+   13     211 bad arithmetic, 221 not a type, 230 not supported yet,
+   14     231 opaque type outside definition.
+   15     Scalar-phase TYPEs (named, integer subrange, enum) check fully;
+   16     composite forms wait for step 3.    Procedure headings (formals,
+   17     result, FORWARD) enter scopes now; bodies parse + check with one
+   18     230 at END (lowering = step 4).
+   19     Statements: assignment, IF/ELSIF/ELSE, WHILE, REPEAT/UNTIL,
+   20     LOOP/EXIT (230 outside LOOP), FOR/TO/static-sign-BY, full CASE
+   21     (labels, ranges, ELSE; compare-chain), RETURN with 232 checks
+   22     (outside proc / value mismatch / missing value). WITH waits for
+   23     records (step 3). Boolean connectives are eager (or/and/xor);
+   24     relations yield 0/1 via cXXw.
+   25     Clarion-form classes (docs/OOP.txt): CLASS decl + single
+   26     inheritance + CLASS IMPLEMENTATION blocks, methods with ";"
+   27     (per the Table example, not the sketch's ","), VIRTUAL flagged.
+   28     Scopes and member checks now; lowering later (one 230 per
+   29     class/impl block). Classic identifiers: no underscores, so the
+   30     Table example's _names stay lexically out of reach. *)
+   31  
+   32  IMPORT SymTab, QbeGen;
+   33  
+   34  CHARACTERS
+   35    eol      = CHR(13) .
+   36    lf       = CHR(10) .
+   37    letter   = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" .
+   38    digit    = "0123456789" .
+   39    hexDigit = digit + "ABCDEFabcdef" .
+   40    noQuote1 = ANY - "'" - eol .
+   41    noQuote2 = ANY - '"' - eol .
+   42  
+   43  IGNORE CHR(9) .. CHR(13)
+   44  
+   45  COMMENTS FROM "(*" TO "*)" NESTED
+   46  COMMENTS FROM "//" TO lf
+   47  
+   48  TOKENS
+   49    ident   = letter { letter | digit } .
+   50    integer = digit { digit }
+   51            | digit { digit } CONTEXT("..")
+   52            | "0x" hexDigit { hexDigit }
+   53            | "0X" hexDigit { hexDigit } .
+   54    real    = digit { digit } "." { digit }
+   55              [ ( "E" | "e" ) [ "+" | "-" ] digit { digit } ] .
+   56    string  = "'" { noQuote1 } "'"
+   57            | '"' { noQuote2 } '"' .
+   58  
+   59  PRODUCTIONS
+   60    M2
+   61      = Unit "." .
+   62    (* Units: program modules compile fully; DEFINITION and
+   63       IMPLEMENTATION modules parse + check now but lower in step 4
+   64       (each ends with one 230); same for nested local modules. *)
+   65    Unit
+   66      = DefUnit
+   67      | ImplUnit
+   68      | ProgModule .
+   69    DefUnit                               (. VAR m1, m2, pn: SymTab.Name; .)
+   70      = "DEFINITION" "MODULE"
+   71        GetIdent<m1>                      (. SymTab.Init; QbeGen.OpenModule(m1);
+   72                                             IF ~SymTab.Enter(m1,
+   73                                                SymTab.KindModule) THEN
+   74                                               SemError(200) END; .)
+   75        ";"
+   76        { Import }
+   77        { ConstBlock | TypeBlock<TRUE> | VarBlock
+   78        | ProcHeading<pn> ";"             (. SymTab.CloseProc; .) }
+   79        "END"
+   80        GetIdent<m2>                      (. IF ~SymTab.Equal(m1, m2) THEN
+   81                                               SemError(202) END; .)
+   82                                          (. QbeGen.CloseModule;
+   83                                             SemError(230);
+   84                                             SymTab.PrintTable; .) .
+   85    ImplUnit                              (. VAR m1, m2: SymTab.Name; .)
+   86      = "IMPLEMENTATION" "MODULE"
+   87        GetIdent<m1>                      (. SymTab.Init; QbeGen.OpenModule(m1);
+   88                                             IF ~SymTab.Enter(m1,
+   89                                                SymTab.KindModule) THEN
+   90                                               SemError(200) END; .)
+   91        ";"
+   92        { Import }
+   93        DeclSeq
+   94        [ "BEGIN"                         (. QbeGen.BeginBody; .)
+   95          [ StatSeq ] ]
+   96        "END"
+   97        GetIdent<m2>                      (. IF ~SymTab.Equal(m1, m2) THEN
+   98                                               SemError(202) END; .)
+   99                                          (. QbeGen.CloseModule;
+  100                                             SemError(230);
+  101                                             SymTab.PrintTable; .) .
+  102    ProgModule                            (. VAR m1, m2: SymTab.Name; .)
+  103      = "MODULE"
+  104        GetIdent<m1>                      (. SymTab.Init; QbeGen.OpenModule(m1);
+  105                                             IF ~SymTab.Enter(m1,
+  106                                                SymTab.KindModule) THEN
+  107                                               SemError(200) END; .)
+  108        [ Priority ]
+  109        ";"
+  110        { Import }
+  111        DeclSeq
+  112        [ "BEGIN"                         (. QbeGen.BeginBody; .)
+  113          [ StatSeq ] ]
+  114        "END"
+  115        GetIdent<m2>                      (. IF ~SymTab.Equal(m1, m2) THEN
+  116                                               SemError(202) END; .)
+  117                                          (. QbeGen.EndModule;
+  118                                             SymTab.PrintTable; .) .
+  119    DeclSeq
+  120      = { ConstBlock | TypeBlock<FALSE> | VarBlock | ProcDecl ";"
+  121        | NestedModule ";" | ClassItem ";" } .
+  122    (* Local module, Wirth form. Parses + checks fully (own scope);
+  123       lowering waits for step 4, marked by one 230 at END. *)
+  124    NestedModule                          (. VAR m1, m2: SymTab.Name; .)
+  125      = "MODULE"
+  126        GetIdent<m1>                      (. IF ~SymTab.Enter(m1,
+  127                                               SymTab.KindModule) THEN
+  128                                               SemError(200) END;
+  129                                             SymTab.PushScope; .)
+  130        [ Priority ]
+  131        ";"
+  132        { Import }
+  133        [ Export ]
+  134        DeclSeq
+  135        [ "BEGIN"
+  136          [ StatSeq ] ]
+  137        "END"
+  138        GetIdent<m2>                      (. IF ~SymTab.Equal(m1, m2) THEN
+  139                                               SemError(202) END;
+  140                                             SymTab.PopScope;
+  141                                             SemError(230); .) .
+  142    Priority
+  143      = "[" integer "]"                   (. SemError(230); .) .
+  144    Export                                (. VAR nm: SymTab.Name; .)
+  145      = "EXPORT"                          (* names wire up in step 4 *)
+  146        [ "QUALIFIED" ]
+  147        GetIdent<nm>
+  148        { "," GetIdent<nm> }
+  149        ";" .
+  150    Import                                (. VAR n: SymTab.Name; .)
+  151      = "FROM"
+  152        GetIdent<n>                       (. IF ~SymTab.Enter(n,
+  153                                               SymTab.KindImport) THEN
+  154                                               SemError(200) END; .)
+  155        "IMPORT"
+  156        ImpList ";"
+  157      | "IMPORT"
+  158        ImpList ";" .
+  159    ImpList                               (. VAR n: SymTab.Name; .)
+  160      = GetIdent<n>                       (. IF ~SymTab.Enter(n,
+  161                                               SymTab.KindImport) THEN
+  162                                               SemError(200) END; .)
+  163        { ","
+  164          GetIdent<n>                     (. IF ~SymTab.Enter(n,
+  165                                               SymTab.KindImport) THEN
+  166                                               SemError(200) END; .) } .
+  167    (* Opaque TYPE declarations (definition modules). The targetless
+  168       alias resolves to InvalidType until step 4 completes it. *)
+  169    (* Scalar-phase TYPEs: named types, integer subranges, enumerations.
+  170       Opaque "TYPE T;" needs isDef (definition units); elsewhere 231.
+  171       Composite forms (ARRAY/RECORD/SET/POINTER) arrive with step 3. *)
+  172    TypeBlock<isDef: BOOLEAN>
+  173      = "TYPE" { TypeItem<isDef> ";" | ClassItem ";" } .
+  174    TypeItem<isDef: BOOLEAN>              (. VAR n: SymTab.Name;
+  175                                               t: SymTab.TypeIndex; .)
+  176      = GetIdent<n>                       (. IF ~SymTab.Enter(n,
+  177                                               SymTab.KindType) THEN
+  178                                               SemError(200) END; .)
+  179        ( "=" Type<t>                     (. SymTab.SetSymType(n, t); .)
+  180        |                                 (. IF ~isDef THEN
+  181                                               SemError(231) END;
+  182                                             SymTab.SetSymType(n,
+  183                                               SymTab.NewAlias()); .) ) .
+  184    Type<VAR t: SymTab.TypeIndex>
+  185      = TypeIdent<t>
+  186      | Subrange<t>
+  187      | Enum<t> .
+  188    TypeIdent<VAR t: SymTab.TypeIndex>    (. VAR n: SymTab.Name;
+  189                                               k: INTEGER; .)
+  190      = GetIdent<n>                       (. IF ~SymTab.Lookup(n) THEN
+  191                                               SemError(201);
+  192                                               t := SymTab.InvalidType
+  193                                             ELSE k := SymTab.SymKind(n);
+  194                                               IF (k = SymTab.KindType)
+  195                                                  OR (k = SymTab.KindPredef) THEN
+  196                                                 t := SymTab.SymType(n)
+  197                                               ELSIF k = SymTab.KindImport THEN
+  198                                                 t := SymTab.InvalidType
+  199                                               ELSE SemError(221);
+  200                                                 t := SymTab.InvalidType
+  201                                               END
+  202                                             END; .) .
+  203    Subrange<VAR t: SymTab.TypeIndex>     (. VAR tlo, thi: SymTab.TypeIndex;
+  204                                               qlo, qhi: QbeGen.QVal;
+  205                                               lo, hi: INTEGER; .)
+  206      = "[" Expr<tlo, qlo> ".." Expr<thi, qhi>
+  207                                          (. IF (tlo = SymTab.InvalidType)
+  208                                              OR (thi = SymTab.InvalidType) THEN
+  209                                             t := SymTab.InvalidType
+  210                                           ELSIF (SymTab.ClassOf(tlo) #
+  211                                                  SymTab.ClInt)
+  212                                              OR (SymTab.ClassOf(thi) #
+  213                                                  SymTab.ClInt) THEN
+  214                                             SemError(230);
+  215                                             t := SymTab.InvalidType
+  216                                           ELSIF ~SymTab.ConstInt(qlo, lo)
+  217                                              OR ~SymTab.ConstInt(qhi, hi)
+  218                                              OR (lo > hi) THEN
+  219                                             SemError(230);
+  220                                             t := SymTab.InvalidType
+  221                                           ELSE t := SymTab.NewSubR(lo, hi)
+  222                                           END; .)
+  223        "]" .
+  224    Enum<VAR t: SymTab.TypeIndex>         (. VAR n: SymTab.Name; .)
+  225      = "("                               (. t := SymTab.NewEnum(); .)
+  226        GetIdent<n>                       (. IF ~SymTab.Enter(n,
+  227                                               SymTab.KindConst) THEN
+  228                                               SemError(200) END;
+  229                                             SymTab.SetSymType(n, t); .)
+  230        { "," GetIdent<n>                 (. IF ~SymTab.Enter(n,
+  231                                               SymTab.KindConst) THEN
+  232                                               SemError(200) END;
+  233                                             SymTab.SetSymType(n, t); .) }
+  234        ")" .
+  235    (* Clarion-form classes (docs/OOP.txt): declaration + single
+  236       inheritance + IMPLEMENTATION blocks. Scopes and member checks
+  237       now; lowering (vtable, dispatch, THIS) later — one 230 per
+  238       class/impl block. Methods end with ";" per the Table example
+  239       (not "," as in the sketch). No underscores in identifiers. *)
+  240    (* Single CLASS item in both loops: separating declaration from
+  241       IMPLEMENTATION at the loop level needs 2-token lookahead
+  242       (CLASS ident vs CLASS IMPLEMENTATION), which LL(1) cannot do.
+  243       The second token decides after CLASS is consumed. A misplaced
+  244       CLASS IMPLEMENTATION inside TYPE still parses (harmless: the
+  245       whole unit ends 230 until lowering). *)
+  246    ClassItem
+  247      = "CLASS" ( "IMPLEMENTATION" ClassImplRest | ClassRest ) .
+  248    ClassRest                             (. VAR cn, m2, pn: SymTab.Name;
+  249                                               ct: SymTab.TypeIndex; .)
+  250      = GetIdent<cn>                      (. IF ~SymTab.Enter(cn,
+  251                                               SymTab.KindType) THEN
+  252                                               SemError(200) END;
+  253                                             ct := SymTab.NewClass();
+  254                                             SymTab.SetSymType(cn, ct);
+  255                                             SymTab.PushClassScope(ct); .)
+  256        [ Parents<ct> ]
+  257        ";"
+  258        { ClassField<ct> ";" }
+  259        { MethodHeading<pn> ";"           (. SymTab.CloseProc; .) }
+  260        "END"
+  261        GetIdent<m2>                      (. IF ~SymTab.Equal(cn, m2) THEN
+  262                                               SemError(202) END;
+  263                                             SymTab.PopScope;
+  264                                             SemError(230); .) .
+  265    Parents<ct: SymTab.TypeIndex>         (. VAR p: SymTab.Name; .)
+  266      = "(" Parent1<ct>
+  267        { "," GetIdent<p>                 (. SemError(230); .) }
+  268        ")" .
+  269    Parent1<ct: SymTab.TypeIndex>         (. VAR p: SymTab.Name;
+  270                                               pt: SymTab.TypeIndex; .)
+  271      = GetIdent<p>                       (. IF ~SymTab.Lookup(p) THEN
+  272                                               SemError(201)
+  273                                             ELSE pt := SymTab.SymType(p);
+  274                                               IF SymTab.ClassOf(pt) #
+  275                                                  SymTab.ClClass THEN
+  276                                                 SemError(230)
+  277                                               ELSE SymTab.SetParent(ct, pt)
+  278                                               END
+  279                                             END; .) .
+  280    ClassField<ct: SymTab.TypeIndex>      (. VAR n, rhs: SymTab.Name;
+  281                                               t: SymTab.TypeIndex; .)
+  282      = GetIdent<n>
+  283        ( "=" GetIdent<rhs>               (. IF ~SymTab.Enter(n,
+  284                                               SymTab.KindConst) THEN
+  285                                               SemError(200) END;
+  286                                             IF SymTab.Lookup(rhs) THEN
+  287                                               SymTab.SetSymType(n,
+  288                                                 SymTab.SymType(rhs))
+  289                                             END; .)
+  290        |                                 (. IF ~SymTab.FieldPending(ct,
+  291                                               n) THEN
+  292                                               SemError(200) END; .)
+  293          { "," GetIdent<n>               (. IF ~SymTab.FieldPending(ct,
+  294                                               n) THEN
+  295                                               SemError(200) END; .) }
+  296          ":" TypeIdent<t>                (. SymTab.FixPendingF(ct, t); .) ) .
+  297    MethodHeading<VAR pn: SymTab.Name>    (. VAR wantVirt: BOOLEAN; .)
+  298      =                                   (. wantVirt := FALSE; .)
+  299        [ "VIRTUAL"                       (. wantVirt := TRUE; .) ]
+  300        ProcHeading<pn>                   (. IF wantVirt THEN
+  301                                               SymTab.MarkVirtual END; .) .
+  302    ClassImplRest                         (. VAR cn, m2: SymTab.Name;
+  303                                               ct: SymTab.TypeIndex; .)
+  304      = GetIdent<cn>                      (. IF ~SymTab.Lookup(cn) THEN
+  305                                               SemError(201);
+  306                                               ct := SymTab.InvalidType
+  307                                             ELSE ct := SymTab.SymType(cn);
+  308                                               IF SymTab.ClassOf(ct) #
+  309                                                  SymTab.ClClass THEN
+  310                                                 SemError(230);
+  311                                                 ct := SymTab.InvalidType
+  312                                               END
+  313                                             END;
+  314                                             IF ct #
+  315                                                SymTab.InvalidType THEN
+  316                                               IF ~SymTab.PushClassMembers(
+  317                                                      ct) THEN
+  318                                                 SemError(230) END
+  319                                             END; .)
+  320        ";" { MethodImpl<ct> ";" }
+  321        [ "BEGIN"
+  322          [ StatSeq ] ]
+  323        "END"
+  324        GetIdent<m2>                      (. IF ~SymTab.Equal(cn, m2) THEN
+  325                                               SemError(202) END;
+  326                                             SymTab.PopScope;
+  327                                             SemError(230); .) .
+  328    MethodImpl<ct: SymTab.TypeIndex>      (. VAR pn: SymTab.Name; .)
+  329      = MethodHeading<pn> ";"
+  330                                          (. IF (ct #
+  331                                                SymTab.InvalidType)
+  332                                               & ~SymTab.MethodExists(ct,
+  333                                                    pn) THEN
+  334                                               SemError(201) END; .)
+  335        ( "FORWARD"                       (. SymTab.MarkFwd;
+  336                                             SymTab.CloseProc; .)
+  337        | Block<pn>                       (. SymTab.CloseProc;
+  338                                             SemError(230); .) ) .
+  339    ConstBlock
+  340      = "CONST" { ConstDecl ";" } .
+  341    ConstDecl                             (. VAR n: SymTab.Name;
+  342                                               t: SymTab.TypeIndex;
+  343                                               qv: QbeGen.QVal;
+  344                                               cls: INTEGER; .)
+  345      = GetIdent<n>                       (. IF ~SymTab.Enter(n,
+  346                                               SymTab.KindConst) THEN
+  347                                               SemError(200) END; .)
+  348        "="
+  349        Expr<t, qv>                       (. SymTab.SetSymType(n, t);
+  350                                             cls := SymTab.ClassOf(t);
+  351                                             IF cls = SymTab.ClStr THEN
+  352                                               SemError(230)
+  353                                             ELSIF ~QbeGen.IsImm(qv) THEN
+  354                                               SemError(230) END;
+  355                                             QbeGen.DeclConst(n, qv, t); .) .
+  356    VarBlock
+  357      = "VAR" { VarDecl ";" } .
+  358    VarDecl                               (. VAR nm: SymTab.Name;
+  359                                               t: SymTab.TypeIndex;
+  360                                               i: CARDINAL;
+  361                                               cls: INTEGER; .)
+  362      = VarIdents ":"
+  363        TypeIdent<t>                      (. cls := SymTab.ClassOf(t);
+  364                                             IF (t # SymTab.InvalidType)
+  365                                                & (cls # SymTab.ClInt)
+  366                                                & (cls # SymTab.ClBool)
+  367                                                & (cls # SymTab.ClChar)
+  368                                                & (cls # SymTab.ClReal) THEN
+  369                                               SemError(230) END;
+  370                                             i := 0;
+  371                                             WHILE i < SymTab.PendCount() DO
+  372                                               SymTab.PendName(i, nm);
+  373                                               QbeGen.DeclVar(nm, t);
+  374                                               INC(i)
+  375                                             END;
+  376                                             SymTab.FixPending(t); .) .
+  377    VarIdents                             (. VAR n: SymTab.Name; .)
+  378      = GetIdent<n>                       (. IF ~SymTab.EnterPending(n,
+  379                                               SymTab.KindVar) THEN
+  380                                               SemError(200) END; .)
+  381        { ","
+  382          GetIdent<n>                     (. IF ~SymTab.EnterPending(n,
+  383                                               SymTab.KindVar) THEN
+  384                                               SemError(200) END; .) } .
+  385    ParIdents<isV: BOOLEAN>               (. VAR n: SymTab.Name; .)
+  386      = GetIdent<n>                       (. IF ~SymTab.EnterParam(n, isV) THEN
+  387                                               SemError(200) END; .)
+  388        { "," GetIdent<n>                 (. IF ~SymTab.EnterParam(n, isV) THEN
+  389                                               SemError(200) END; .) } .
+  390    (* Procedure headings enter now (scopes, params, result); bodies
+  391       parse + check with one 230 at END (lowering = step 4).
+  392       FORWARD marks; the body heading re-enters (signature compare
+  393       tightens in step 4). *)
+  394    ProcHeading<VAR pn: SymTab.Name>      (. VAR t: SymTab.TypeIndex; .)
+  395      = "PROCEDURE"
+  396        GetIdent<pn>                      (. IF ~SymTab.EnterProc(pn) THEN
+  397                                             IF ~SymTab.ReenterProc(pn) THEN
+  398                                               SemError(200) END
+  399                                             END; .)
+  400        [ FormalParams ]
+  401        [ ":" TypeIdent<t>                (. SymTab.SetProcRes(t); .) ] .
+  402    FormalParams
+  403      = "(" [ ParamSection { ";" ParamSection } ] ")" .
+  404    ParamSection                          (. VAR t: SymTab.TypeIndex;
+  405                                               isV: BOOLEAN; .)
+  406      =                                   (. isV := FALSE; .)
+  407        [ "VAR"                           (. isV := TRUE; .) ]
+  408        ParIdents<isV> ":" TypeIdent<t>   (. SymTab.FixPending(t); .) .
+  409    ProcDecl                              (. VAR pn, m2: SymTab.Name; .)
+  410      = ProcHeading<pn> ";"
+  411        ( "FORWARD"                       (. SymTab.MarkFwd;
+  412                                             SymTab.CloseProc; .)
+  413        | Block<pn>                       (. SymTab.CloseProc;
+  414                                             SemError(230); .) ) .
+  415    Block<pn: SymTab.Name>                (. VAR m2: SymTab.Name; .)
+  416      = DeclSeq
+  417        [ "BEGIN"
+  418          [ StatSeq ] ]
+  419        "END"
+  420        GetIdent<m2>                      (. IF ~SymTab.Equal(pn, m2) THEN
+  421                                               SemError(202) END; .) .
+  422    StatSeq
+  423      = Statement { ";" Statement } .
+  424    Statement                             (. VAR lx: QbeGen.QVal; .)
+  425      = Assign
+  426      | IfStat
+  427      | WhileStat
+  428      | RepeatStat
+  429      | LoopStat
+  430      | ForStat
+  431      | CaseStat
+  432      | ReturnStat
+  433      | "EXIT"                            (. IF QbeGen.TopLoop(lx) THEN
+  434                                               QbeGen.Jmp(lx)
+  435                                             ELSE SemError(230) END; .) .
+  436    Assign                                (. VAR dt, et: SymTab.TypeIndex;
+  437                                               dk: INTEGER;
+  438                                               qd, qe, qt: QbeGen.QVal;
+  439                                               qn: SymTab.Name;
+  440                                               isR, conv: BOOLEAN; .)
+  441      = Design<dt, dk, qd, qn> ":="
+  442        Expr<et, qe>                      (. IF (dt # SymTab.InvalidType)
+  443                                             & (dk # SymTab.KindVar)
+  444                                             & (dk # SymTab.KindParam) THEN
+  445                                             SemError(210)
+  446                                           ELSIF ~SymTab.Assignable(et,
+  447                                                    dt) THEN
+  448                                             SemError(210) END;
+  449                                           isR := (dt #
+  450                                                   SymTab.InvalidType)
+  451                                                  & (SymTab.ClassOf(dt)
+  452                                                     = SymTab.ClReal);
+  453                                           conv := isR
+  454                                                   & SymTab.IsIntFamily(et);
+  455                                           IF ((dk = SymTab.KindVar)
+  456                                               OR (dk = SymTab.KindParam))
+  457                                              & (dt # SymTab.InvalidType)
+  458                                              & (et # SymTab.InvalidType) THEN
+  459                                             IF conv THEN
+  460                                               QbeGen.ConvIR(qe, qt);
+  461                                               QbeGen.StoreVar(qn, qt, TRUE)
+  462                                             ELSE
+  463                                               QbeGen.StoreVar(qn, qe, isR)
+  464                                             END
+  465                                           END; .) .
+  466    IfStat                                (. VAR t: SymTab.TypeIndex;
+  467                                               q, lThen, lElse, lEnd:
+  468                                                 QbeGen.QVal;
+  469                                               hasElse: BOOLEAN; .)
+  470      = "IF"                              (. hasElse := FALSE; .)
+  471        Expr<t, q>                        (. IF ~SymTab.BoolCheck(t) THEN
+  472                                               SemError(214) END;
+  473                                             QbeGen.NewLabel(lThen);
+  474                                             QbeGen.NewLabel(lElse);
+  475                                             QbeGen.NewLabel(lEnd);
+  476                                             QbeGen.Jnz(q, lThen, lElse);
+  477                                             QbeGen.EmitLabel(lThen); .)
+  478        "THEN" StatSeq                    (. QbeGen.Jmp(lEnd); .)
+  479        { "ELSIF"                         (. QbeGen.EmitLabel(lElse);
+  480                                             QbeGen.NewLabel(lElse); .)
+  481          Expr<t, q>                      (. IF ~SymTab.BoolCheck(t) THEN
+  482                                               SemError(214) END;
+  483                                             QbeGen.NewLabel(lThen);
+  484                                             QbeGen.Jnz(q, lThen, lElse);
+  485                                             QbeGen.EmitLabel(lThen); .)
+  486          "THEN" StatSeq                  (. QbeGen.Jmp(lEnd); .) }
+  487        [ "ELSE"                          (. QbeGen.EmitLabel(lElse);
+  488                                             hasElse := TRUE; .)
+  489          StatSeq ]
+  490        "END"                             (. IF hasElse THEN
+  491                                               QbeGen.EmitLabel(lEnd)
+  492                                             ELSE QbeGen.EmitLabel(lElse);
+  493                                               QbeGen.EmitLabel(lEnd)
+  494                                             END; .) .
+  495    WhileStat                             (. VAR t: SymTab.TypeIndex;
+  496                                               q, lTop, lBody, lEnd:
+  497                                                 QbeGen.QVal; .)
+  498      = "WHILE"                           (. QbeGen.NewLabel(lTop);
+  499                                             QbeGen.NewLabel(lBody);
+  500                                             QbeGen.NewLabel(lEnd);
+  501                                             QbeGen.EmitLabel(lTop); .)
+  502        Expr<t, q>                        (. IF ~SymTab.BoolCheck(t) THEN
+  503                                               SemError(214) END;
+  504                                             QbeGen.Jnz(q, lBody, lEnd);
+  505                                             QbeGen.EmitLabel(lBody); .)
+  506        "DO" StatSeq                      (. QbeGen.Jmp(lTop); .)
+  507        "END"                             (. QbeGen.EmitLabel(lEnd); .) .
+  508    RepeatStat                            (. VAR t: SymTab.TypeIndex;
+  509                                               q, lTop, lEnd: QbeGen.QVal; .)
+  510      = "REPEAT"                          (. QbeGen.NewLabel(lTop);
+  511                                             QbeGen.NewLabel(lEnd);
+  512                                             QbeGen.EmitLabel(lTop); .)
+  513        StatSeq
+  514        "UNTIL" Expr<t, q>                (. IF ~SymTab.BoolCheck(t) THEN
+  515                                               SemError(214) END;
+  516                                             QbeGen.Jnz(q, lEnd, lTop);
+  517                                             QbeGen.EmitLabel(lEnd); .) .
+  518    LoopStat                              (. VAR lTop, lEnd: QbeGen.QVal; .)
+  519      = "LOOP"                            (. QbeGen.NewLabel(lTop);
+  520                                             QbeGen.NewLabel(lEnd);
+  521                                             QbeGen.PushLoop(lEnd);
+  522                                             QbeGen.EmitLabel(lTop); .)
+  523        StatSeq
+  524        "END"                             (. QbeGen.Jmp(lTop);
+  525                                             QbeGen.PopLoop;
+  526                                             QbeGen.EmitLabel(lEnd); .) .
+  527    (* FOR with static-sign BY (literal, non-zero; 220 otherwise).
+  528       Runtime direction would need a compare-select; the literal
+  529       sign picks cslew/csegew at "DO" time. *)
+  530    ForStat                               (. VAR lv: SymTab.Name;
+  531                                               tlo, thi, tby:
+  532                                                 SymTab.TypeIndex;
+  533                                               qlo, qhi, qby, qt, qk, qb:
+  534                                                 QbeGen.QVal;
+  535                                               lTop, lBody, lEnd:
+  536                                                 QbeGen.QVal;
+  537                                               by: INTEGER;
+  538                                               ok: BOOLEAN; .)
+  539      = "FOR"                             (. by := 1; .)
+  540        GetIdent<lv>                      (. ok := SymTab.Lookup(lv);
+  541                                             IF ~ok THEN
+  542                                               SemError(201)
+  543                                             ELSIF (SymTab.SymKind(lv) #
+  544                                                    SymTab.KindVar)
+  545                                                & (SymTab.SymKind(lv) #
+  546                                                   SymTab.KindParam) THEN
+  547                                               SemError(220); ok := FALSE
+  548                                             ELSIF ~SymTab.IsIntFamily(
+  549                                                     SymTab.SymType(lv)) THEN
+  550                                               SemError(220); ok := FALSE
+  551                                             END; .)
+  552        ":=" Expr<tlo, qlo>               (. IF ~SymTab.IsIntFamily(tlo) THEN
+  553                                               SemError(220); ok := FALSE
+  554                                             END; .)
+  555        "TO" Expr<thi, qhi>               (. IF ~SymTab.IsIntFamily(thi) THEN
+  556                                               SemError(220); ok := FALSE
+  557                                             END; .)
+  558        [ "BY" Expr<tby, qby>             (. IF (tby #
+  559                                               SymTab.InvalidType)
+  560                                              & ~SymTab.IsIntFamily(tby) THEN
+  561                                               SemError(220); ok := FALSE
+  562                                             END;
+  563                                             IF ~SymTab.ConstInt(qby, by) THEN
+  564                                               SemError(230); by := 1
+  565                                             ELSIF by = 0 THEN
+  566                                               SemError(220); by := 1
+  567                                             END; .) ]
+  568        "DO"                              (. IF ok THEN
+  569                                               QbeGen.StoreVar(lv, qlo,
+  570                                                 FALSE) END;
+  571                                             QbeGen.NewLabel(lTop);
+  572                                             QbeGen.NewLabel(lBody);
+  573                                             QbeGen.NewLabel(lEnd);
+  574                                             QbeGen.EmitLabel(lTop);
+  575                                             QbeGen.LoadVar(lv, FALSE, qt);
+  576                                             QbeGen.NewTemp(qk);
+  577                                             IF by > 0 THEN
+  578                                               QbeGen.Op3("cslew", qk,
+  579                                                 qt, qhi, FALSE)
+  580                                             ELSE QbeGen.Op3("csgew", qk,
+  581                                               qt, qhi, FALSE)
+  582                                             END;
+  583                                             QbeGen.Jnz(qk, lBody, lEnd);
+  584                                             QbeGen.EmitLabel(lBody); .)
+  585        StatSeq
+  586        "END"                             (. IF ok THEN
+  587                                               QbeGen.LoadVar(lv, FALSE,
+  588                                                 qt);
+  589                                               QbeGen.IntStr(by, qb);
+  590                                               QbeGen.NewTemp(qk);
+  591                                               QbeGen.Op3("add", qk,
+  592                                                 qt, qb, FALSE);
+  593                                               QbeGen.StoreVar(lv, qk,
+  594                                                 FALSE) END;
+  595                                             QbeGen.Jmp(lTop);
+  596                                             QbeGen.EmitLabel(lEnd); .) .
+  597    CaseStat                              (. VAR tsel: SymTab.TypeIndex;
+  598                                               qsel, lEnd: QbeGen.QVal; .)
+  599      = "CASE" Expr<tsel, qsel>           (. QbeGen.NewLabel(lEnd); .)
+  600        "OF" CaseAlt<tsel, qsel, lEnd>
+  601        { "|" CaseAlt<tsel, qsel, lEnd> }
+  602        [ "ELSE" StatSeq ]
+  603        "END"                             (. QbeGen.EmitLabel(lEnd); .) .
+  604    (* Compare-chain lowering: each alternative ends its match-tests
+  605       with "jmp lAfter", so the no-match fallthrough skips the body:
+  606       "cmp; jnz(lBody,lF); lF: ... ; jmp lAfter; lBody: S; jmp lEnd;
+  607       lAfter:". Falls into the next alternative, ELSE, or END. *)
+  608    CaseAlt<tsel: SymTab.TypeIndex; qsel: QbeGen.QVal;
+  609            lEnd: QbeGen.QVal>            (. VAR lBody, lAfter: QbeGen.QVal; .)
+  610      =                                   (. QbeGen.NewLabel(lBody);
+  611                                             QbeGen.NewLabel(lAfter); .)
+  612        CaseLabel<tsel, qsel, lBody>
+  613        { "," CaseLabel<tsel, qsel, lBody> }
+  614        ":"                               (. QbeGen.Jmp(lAfter);
+  615                                             QbeGen.EmitLabel(lBody); .)
+  616        StatSeq                           (. QbeGen.Jmp(lEnd);
+  617                                             QbeGen.EmitLabel(lAfter); .) .
+  618    CaseLabel<tsel: SymTab.TypeIndex; qsel: QbeGen.QVal;
+  619              lBody: QbeGen.QVal>         (. VAR t2, t3: SymTab.TypeIndex;
+  620                                               q2, q3, qc, qd, qe:
+  621                                                 QbeGen.QVal;
+  622                                               lNext: QbeGen.QVal; .)
+  623      = Expr<t2, q2>                      (. IF (t2 #
+  624                                               SymTab.InvalidType)
+  625                                              & (tsel #
+  626                                                 SymTab.InvalidType)
+  627                                              & ~SymTab.EqCheck(t2,
+  628                                                   tsel) THEN
+  629                                               SemError(213) END;
+  630                                             IF ~QbeGen.IsImm(q2) THEN
+  631                                               SemError(230);
+  632                                               QbeGen.CopyOp("0", q2)
+  633                                             END;
+  634                                             QbeGen.NewLabel(lNext);
+  635                                             QbeGen.Cmp(SymTab.OpEq,
+  636                                               qsel, q2, qc, FALSE);
+  637                                             QbeGen.Jnz(qc, lBody, lNext);
+  638                                             QbeGen.EmitLabel(lNext); .)
+  639        [ ".." Expr<t3, q3>               (. IF (t3 #
+  640                                               SymTab.InvalidType)
+  641                                              & (tsel #
+  642                                                 SymTab.InvalidType)
+  643                                              & ~SymTab.EqCheck(t3,
+  644                                                   tsel) THEN
+  645                                               SemError(213) END;
+  646                                             IF ~QbeGen.IsImm(q3) THEN
+  647                                               SemError(230);
+  648                                               QbeGen.CopyOp("0", q3)
+  649                                             END;
+  650                                             QbeGen.Cmp(SymTab.OpGe,
+  651                                               qsel, q2, qc, FALSE);
+  652                                             QbeGen.Cmp(SymTab.OpLe,
+  653                                               qsel, q3, qd, FALSE);
+  654                                             QbeGen.NewTemp(qe);
+  655                                             QbeGen.Op3("and", qe, qc, qd,
+  656                                               FALSE);
+  657                                             QbeGen.NewLabel(lNext);
+  658                                             QbeGen.Jnz(qe, lBody, lNext);
+  659                                             QbeGen.EmitLabel(lNext); .) ] .
+  660    ReturnStat                            (. VAR t: SymTab.TypeIndex;
+  661                                               q: QbeGen.QVal;
+  662                                               res: SymTab.TypeIndex;
+  663                                               hadE: BOOLEAN; .)
+  664      = "RETURN"                          (. hadE := FALSE; .)
+  665        [ Expr<t, q>                      (. hadE := TRUE; .) ]
+  666                                          (. IF ~SymTab.InProc() THEN
+  667                                               SemError(232)
+  668                                             ELSE res := SymTab.CurRes();
+  669                                               IF ~hadE THEN
+  670                                                 IF res #
+  671                                                    SymTab.InvalidType THEN
+  672                                                   SemError(232) END
+  673                                               ELSIF (res =
+  674                                                      SymTab.InvalidType)
+  675                                                  OR (t #
+  676                                                      SymTab.InvalidType)
+  677                                                  & ~SymTab.Assignable(t,
+  678                                                       res) THEN
+  679                                                 SemError(232)
+  680                                               END
+  681                                             END; .) .
+  682    Design<VAR t: SymTab.TypeIndex; VAR k: INTEGER;
+  683           VAR q: QbeGen.QVal; VAR qn: SymTab.Name>
+  684                                          (. VAR n: SymTab.Name;
+  685                                               cls: INTEGER; .)
+  686      = GetIdent<n>                       (. QbeGen.CopyOp(n, qn);
+  687                                             IF ~SymTab.Lookup(n) THEN
+  688                                               SemError(201);
+  689                                               t := SymTab.InvalidType;
+  690                                               k := -1;
+  691                                               QbeGen.CopyOp("0", q)
+  692                                             ELSE
+  693                                               t := SymTab.SymType(n);
+  694                                               k := SymTab.SymKind(n);
+  695                                               IF k = SymTab.KindConst THEN
+  696                                                 IF SymTab.Equal(n,
+  697                                                    "TRUE") THEN
+  698                                                   t := SymTab.BoolType();
+  699                                                   QbeGen.CopyOp("1", q)
+  700                                                 ELSIF SymTab.Equal(n,
+  701                                                    "FALSE") THEN
+  702                                                   t := SymTab.BoolType();
+  703                                                   QbeGen.CopyOp("0", q)
+  704                                                 ELSE
+  705                                                   cls :=
+  706                                                     SymTab.ClassOf(t);
+  707                                                   IF (t #
+  708                                                       SymTab.InvalidType)
+  709                                                      & ((cls = SymTab.ClInt)
+  710                                                      OR (cls
+  711                                                          = SymTab.ClChar)
+  712                                                      OR (cls
+  713                                                          = SymTab.ClReal)) THEN
+  714                                                     QbeGen.LoadVar(n,
+  715                                                       cls = SymTab.ClReal, q)
+  716                                                   ELSE
+  717                                                     IF t #
+  718                                                        SymTab.InvalidType THEN
+  719                                                       SemError(230)
+  720                                                     END;
+  721                                                     QbeGen.CopyOp("0", q)
+  722                                                   END
+  723                                                 END
+  724                                                ELSIF (k = SymTab.KindVar)
+  725                                                   OR (k = SymTab.KindParam) THEN
+  726                                                  cls :=
+  727                                                    SymTab.ClassOf(t);
+  728                                                  IF (cls = SymTab.ClInt)
+  729                                                     OR (cls = SymTab.ClBool)
+  730                                                     OR (cls = SymTab.ClChar)
+  731                                                     OR (cls
+  732                                                         = SymTab.ClReal) THEN
+  733                                                    QbeGen.LoadVar(n,
+  734                                                      cls = SymTab.ClReal, q)
+  735                                                  ELSE SemError(230);
+  736                                                    QbeGen.CopyOp("0", q)
+  737                                                  END
+  738                                                ELSE QbeGen.CopyOp("0", q);
+  739                                                  IF (k = SymTab.KindImport)
+  740                                                     OR (k = SymTab.KindProc)
+  741                                                     OR (k
+  742                                                         = SymTab.KindField) THEN
+  743                                                    SemError(230)
+  744                                                  END
+  745                                                END
+  746                                             END; .) .
+  747    Expr<VAR t: SymTab.TypeIndex; VAR q: QbeGen.QVal>
+  748                                          (. VAR t2: SymTab.TypeIndex;
+  749                                               op: INTEGER;
+  750                                               q2, qt: QbeGen.QVal;
+  751                                               isR: BOOLEAN; .)
+  752      = SimExpr<t, q>
+  753        [ Rel<op> SimExpr<t2, q2>
+  754          (. IF SymTab.RelCheck(t, t2, op) THEN
+  755               isR := SymTab.ClassOf(t) = SymTab.ClReal;
+  756               t := SymTab.BoolType()
+  757             ELSE SemError(213); t := SymTab.InvalidType;
+  758               isR := FALSE END;
+  759             IF t # SymTab.InvalidType THEN
+  760               QbeGen.Cmp(op, q, q2, qt, isR);
+  761               QbeGen.CopyOp(qt, q)
+  762             ELSE QbeGen.CopyOp("0", q)
+  763             END; .) ] .
+  764    Rel<VAR op: INTEGER>
+  765      = "="                               (. op := SymTab.OpEq; .)
+  766      | "#"                               (. op := SymTab.OpNeq1; .)
+  767      | "<"                               (. op := SymTab.OpLt; .)
+  768      | "<="                              (. op := SymTab.OpLe; .)
+  769      | ">"                               (. op := SymTab.OpGt; .)
+  770      | ">="                              (. op := SymTab.OpGe; .) .
+  771    SimExpr<VAR t: SymTab.TypeIndex; VAR q: QbeGen.QVal>
+  772                                          (. VAR t2, res2: SymTab.TypeIndex;
+  773                                               op: INTEGER;
+  774                                               q2, qt: QbeGen.QVal;
+  775                                               neg, isR: BOOLEAN; .)
+  776      =                                   (. neg := FALSE; .)
+  777        [ "+" | "-"                       (. neg := TRUE; .) ]
+  778        Term<t, q>                        (. IF neg THEN
+  779                                             IF QbeGen.IsImm(q) THEN
+  780                                               QbeGen.NegFold(q, q)
+  781                                             ELSE QbeGen.NewTemp(qt);
+  782                                               QbeGen.NegQ(q, qt,
+  783                                                 SymTab.ClassOf(t)
+  784                                                 = SymTab.ClReal);
+  785                                               QbeGen.CopyOp(qt, q)
+  786                                             END
+  787                                           END; .)
+  788        { AddOp<op> Term<t2, q2>
+  789          (. IF op = SymTab.OpOr THEN
+  790               IF SymTab.BoolCheck(t) & SymTab.BoolCheck(t2) THEN
+  791                 t := SymTab.BoolType()
+  792               ELSE SemError(212); t := SymTab.InvalidType END;
+  793               IF t # SymTab.InvalidType THEN
+  794                 QbeGen.NewTemp(qt);
+  795                 QbeGen.Op3("or", qt, q, q2, FALSE);
+  796                 QbeGen.CopyOp(qt, q)
+  797               ELSE QbeGen.CopyOp("0", q)
+  798               END
+  799             ELSE
+  800               IF SymTab.ArithCheck(t, t2, FALSE, res2) THEN
+  801                 t := res2
+  802               ELSE SemError(211); t := SymTab.InvalidType END;
+  803               isR := SymTab.ClassOf(t) = SymTab.ClReal;
+  804               IF t # SymTab.InvalidType THEN
+  805                 QbeGen.NewTemp(qt);
+  806                 IF op = SymTab.OpAdd THEN
+  807                   QbeGen.Op3("add", qt, q, q2, isR)
+  808                 ELSE
+  809                   QbeGen.Op3("sub", qt, q, q2, isR)
+  810                 END;
+  811                 QbeGen.CopyOp(qt, q)
+  812               ELSE QbeGen.CopyOp("0", q)
+  813               END
+  814             END; .) } .
+  815    AddOp<VAR op: INTEGER>
+  816      = "+"                               (. op := SymTab.OpAdd; .)
+  817      | "-"                               (. op := SymTab.OpSub; .)
+  818      | "OR"                              (. op := SymTab.OpOr; .) .
+  819    Term<VAR t: SymTab.TypeIndex; VAR q: QbeGen.QVal>
+  820                                          (. VAR t2, res2: SymTab.TypeIndex;
+  821                                               op: INTEGER;
+  822                                               q2, qt: QbeGen.QVal;
+  823                                               isR: BOOLEAN; .)
+  824      = Fact<t, q> { MulOp<op> Fact<t2, q2>
+  825        (. IF op = SymTab.OpAnd THEN
+  826             IF SymTab.BoolCheck(t) & SymTab.BoolCheck(t2) THEN
+  827               t := SymTab.BoolType()
+  828             ELSE SemError(212); t := SymTab.InvalidType END;
+  829             IF t # SymTab.InvalidType THEN
+  830               QbeGen.NewTemp(qt);
+  831               QbeGen.Op3("and", qt, q, q2, FALSE);
+  832               QbeGen.CopyOp(qt, q)
+  833             ELSE QbeGen.CopyOp("0", q)
+  834             END
+  835           ELSE
+  836             IF SymTab.ArithCheck(t, t2,
+  837                  (op = SymTab.OpDiv) OR (op = SymTab.OpMod),
+  838                  res2) THEN t := res2
+  839             ELSE SemError(211); t := SymTab.InvalidType END;
+  840             isR := SymTab.ClassOf(t) = SymTab.ClReal;
+  841             IF t # SymTab.InvalidType THEN
+  842               QbeGen.NewTemp(qt);
+  843               IF op = SymTab.OpTimes THEN
+  844                 QbeGen.Op3("mul", qt, q, q2, isR)
+  845               ELSIF (op = SymTab.OpDiv)
+  846                  OR (op = SymTab.OpSlash) THEN
+  847                 QbeGen.Op3("div", qt, q, q2, isR)
+  848               ELSE
+  849                 QbeGen.Op3("rem", qt, q, q2, isR)
+  850               END;
+  851               QbeGen.CopyOp(qt, q)
+  852             ELSE QbeGen.CopyOp("0", q)
+  853             END
+  854           END; .) } .
+  855    MulOp<VAR op: INTEGER>
+  856      = "*"                               (. op := SymTab.OpTimes; .)
+  857      | "/"                               (. op := SymTab.OpSlash; .)
+  858      | "DIV"                             (. op := SymTab.OpDiv; .)
+  859      | "MOD"                             (. op := SymTab.OpMod; .)
+  860      | "AND"                             (. op := SymTab.OpAnd; .) .
+  861    Fact<VAR t: SymTab.TypeIndex; VAR q: QbeGen.QVal>
+  862                                          (. VAR s: ARRAY [0 .. 255] OF CHAR;
+  863                                               et, dt, t2: SymTab.TypeIndex;
+  864                                               dk: INTEGER;
+  865                                               qd, q2: QbeGen.QVal;
+  866                                               qn: SymTab.Name; .)
+  867      = integer                           (. LexString(s);
+  868                                             QbeGen.NormInt(s, q);
+  869                                             t := SymTab.IntType(); .)
+  870      | real                              (. LexString(s);
+  871                                             QbeGen.NormReal(s, q);
+  872                                             t := SymTab.RealType(); .)
+  873      | string                            (. LexString(s);
+  874                                             IF SymTab.StrLen(s) <= 3 THEN
+  875                                               t := SymTab.CharType();
+  876                                               QbeGen.IntStr(
+  877                                                 QbeGen.CharVal(s), q)
+  878                                             ELSE t := SymTab.NewStr();
+  879                                               SemError(230);
+  880                                               QbeGen.CopyOp("0", q)
+  881                                             END; .)
+  882      | Design<dt, dk, qd, qn>            (. t := dt;
+  883                                             QbeGen.CopyOp(qd, q); .)
+  884      | "(" Expr<et, q> ")"               (. t := et; .)
+  885      | "NOT" Fact<t2, q2>                (. IF SymTab.BoolCheck(t2) THEN
+  886                                               t := SymTab.BoolType()
+  887                                             ELSE SemError(212);
+  888                                               t := SymTab.InvalidType END;
+  889                                             IF t # SymTab.InvalidType THEN
+  890                                               QbeGen.NotQ(q2, q)
+  891                                             ELSE QbeGen.CopyOp("0", q)
+  892                                             END; .) .
+  893    GetIdent<VAR n: SymTab.Name>
+  894      = ident                             (. LexName(n); .) .
+  895  
+  896  END M2.
+
+    0 errors
+
 
 Statistics:
 
-  nr of terminals:        62 (limit   400)
-  nr of non-terminals:    49 (limit   210)
-  nr of pragmas:           0 (limit   438)
-  nr of symbolnodes:     111 (limit   500)
-  nr of graphnodes:      442 (limit  1500)
-  nr of conditionsets:     5 (limit   100)
+  nr of terminals:        64 (limit   400)
+  nr of non-terminals:    57 (limit   210)
+  nr of pragmas:           0 (limit   436)
+  nr of symbolnodes:     121 (limit   500)
+  nr of graphnodes:      521 (limit  1500)
+  nr of conditionsets:     6 (limit   100)
   nr of charactersets:    11 (limit   250)
 
 

+ 26 - 0
compiler/src/SymTab.def

@@ -41,6 +41,7 @@ CONST
   ClSet     = 8;
   ClPtr     = 9;
   ClStr     = 10;
+  ClClass   = 11;
 
   (* operator codes for RelCheck *)
   OpEq = 0; OpNeq1 = 1; OpNeq2 = 2;
@@ -137,6 +138,10 @@ PROCEDURE ProcRes (name: ARRAY OF CHAR): TypeIndex;
 PROCEDURE MarkFwd;
 (* Marks the current procedure FORWARD (body must follow). *)
 
+PROCEDURE MarkVirtual;
+(* Marks the current procedure VIRTUAL (dynamically dispatched;
+   dispatch tables arrive with class lowering). *)
+
 PROCEDURE CloseProc;
 (* Pops the current procedure's scope and its proc-stack entry.
    Replaces bare PopScope at the end of headings (DefUnit),
@@ -149,6 +154,24 @@ PROCEDURE CurRes (): TypeIndex;
 (* Result type of the innermost open procedure,
    InvalidType outside procedures or for proper procedures. *)
 
+(* ---------------- classes (Clarion form, lowering deferred) ---------------- *)
+
+PROCEDURE SetParent (t, p: TypeIndex);
+(* Records p as the (single) parent class of t. Multiple parents
+   are rejected in the grammar with 230. *)
+
+PROCEDURE PushClassScope (t: TypeIndex);
+(* Pushes the body scope of class t and records it on t. *)
+
+PROCEDURE MethodExists (t: TypeIndex; name: ARRAY OF CHAR): BOOLEAN;
+(* TRUE if the class scope of t declares name as a procedure. *)
+
+PROCEDURE PushClassMembers (t: TypeIndex): BOOLEAN;
+(* Pushes a scope with t's fields (KindField) for method bodies in
+   CLASS IMPLEMENTATION. FALSE if t is not a class. Method names
+   are re-entered fresh by each body (copying them would collide);
+   sibling-method references report 201 until step 4 calls. *)
+
 (* ---------------- type descriptors ---------------- *)
 
 PROCEDURE NewAlias (): TypeIndex;
@@ -162,6 +185,9 @@ PROCEDURE NewRecord (): TypeIndex;
 PROCEDURE NewSet (base: TypeIndex): TypeIndex;
 PROCEDURE NewPtr (base: TypeIndex): TypeIndex;
 PROCEDURE NewStr (): TypeIndex;
+PROCEDURE NewClass (): TypeIndex;
+(* Fresh class descriptor (holds fields like a record; methods live
+   in the class scope). Parent and scope via SetParent/PushClassScope. *)
 (* Fresh descriptors; base/elem may be InvalidType. InvalidType is
    returned when the table is full. *)
 

+ 86 - 3
compiler/src/SymTab.mod

@@ -13,6 +13,7 @@ CONST
   FNone = 0; FAlias = 1; FSub = 2; FEnum = 3; FArray = 4;
   FRecord = 5; FSet = 6; FPtr = 7; FStr = 8;
   FInt = 9; FReal = 10; FChar = 11; FBool = 12;
+  FClass = 13;
 
 TYPE
   SymPtr = POINTER TO SymNode;
@@ -29,6 +30,7 @@ TYPE
     plink : SymPtr;     (* next formal, positional chain *)
     isVar : BOOLEAN;    (* KindParam: VAR formal *)
     fwd   : BOOLEAN;    (* KindProc: FORWARD body pending *)
+    virt  : BOOLEAN;    (* KindProc: VIRTUAL method *)
   END;
   ScopeNode = RECORD
     parent : ScopePtr;  (* scope tree link *)
@@ -57,6 +59,8 @@ VAR
   tref : ARRAY [0 .. MaxTypes - 1] OF TypeIndex;
   tlo : ARRAY [0 .. MaxTypes - 1] OF INTEGER;
   thi : ARRAY [0 .. MaxTypes - 1] OF INTEGER;
+  tparent : ARRAY [0 .. MaxTypes - 1] OF TypeIndex;
+  tscope : ARRAY [0 .. MaxTypes - 1] OF ScopePtr;
   nTypes : CARDINAL;
   curProc : SymPtr;   (* heading being declared *)
   curPTail : SymPtr;  (* positional param chain tail *)
@@ -247,7 +251,7 @@ PROCEDURE FindField (rec: TypeIndex; name: ARRAY OF CHAR): FieldPtr;
   VAR f: FieldPtr;
   BEGIN
     IF (rec < 0) OR (rec >= VAL(INTEGER, nTypes)) THEN RETURN NIL END;
-    IF tform[rec] # FRecord THEN RETURN NIL END;
+    IF (tform[rec] # FRecord) & (tform[rec] # FClass) THEN RETURN NIL END;
     f := fields;
     WHILE f # NIL DO
       IF (f^.owner = rec) & Equal(f^.name, name) THEN RETURN f END;
@@ -388,6 +392,32 @@ PROCEDURE NewStr (): TypeIndex;
     RETURN NewDesc(FStr, InvalidType)
   END NewStr;
 
+PROCEDURE NewClass (): TypeIndex;
+  VAR t: TypeIndex;
+  BEGIN
+    t := NewDesc(FClass, InvalidType);
+    IF t # InvalidType THEN
+      tparent[t] := InvalidType; tscope[t] := NIL
+    END;
+    RETURN t
+  END NewClass;
+
+PROCEDURE SetParent (t, p: TypeIndex);
+  BEGIN
+    IF (t >= 0) & (t < VAL(INTEGER, nTypes)) & (tform[t] = FClass) THEN
+      tparent[t] := p
+    END
+  END SetParent;
+
+PROCEDURE PushClassScope (t: TypeIndex);
+  VAR r: TypeIndex;
+  BEGIN
+    r := Resolve(t);
+    IF (r = InvalidType) OR (tform[r] # FClass) THEN RETURN END;
+    PushScope;
+    tscope[r] := curScope
+  END PushClassScope;
+
 PROCEDURE SetTarget (t, base: TypeIndex);
   BEGIN
     IF (t >= 0) & (t < VAL(INTEGER, nTypes)) & (tform[t] = FAlias) THEN
@@ -434,6 +464,7 @@ PROCEDURE ClassOf (t: TypeIndex): INTEGER;
     | FSet : RETURN ClSet
     | FPtr : RETURN ClPtr
     | FStr : RETURN ClStr
+    | FClass : RETURN ClClass
     | FSub : IF tref[r] = InvalidType THEN RETURN ClInt
              ELSE RETURN ClassOf(tref[r]) END
     ELSE RETURN ClInvalid
@@ -485,12 +516,16 @@ PROCEDURE PtrBase (t: TypeIndex): TypeIndex;
   END PtrBase;
 
 PROCEDURE PushRecord (t: TypeIndex): BOOLEAN;
-(* Pushes a scope with t's fields; caller must PopScope afterwards. *)
+(* Pushes a scope with t's fields; caller must PopScope afterwards.
+   Classes share the field machinery (methods stay in the class
+   scope itself). *)
   VAR r: TypeIndex;
     f: FieldPtr;
   BEGIN
     r := Resolve(t);
-    IF (r < 0) OR (tform[r] # FRecord) THEN RETURN FALSE END;
+    IF (r < 0) OR ((tform[r] # FRecord) & (tform[r] # FClass)) THEN
+      RETURN FALSE
+    END;
     PushScope;
     f := fields;
     WHILE f # NIL DO
@@ -504,6 +539,53 @@ PROCEDURE PushRecord (t: TypeIndex): BOOLEAN;
     RETURN TRUE
   END PushRecord;
 
+(* ---------------- classes ---------------- *)
+
+PROCEDURE MethodExists (t: TypeIndex; name: ARRAY OF CHAR): BOOLEAN;
+  VAR r: TypeIndex;
+    s: ScopePtr;
+    node: SymPtr;
+  BEGIN
+    r := Resolve(t);
+    IF (r = InvalidType) OR (tform[r] # FClass) THEN RETURN FALSE END;
+    s := tscope[r];
+    IF s = NIL THEN RETURN FALSE END;
+    node := TreeFind(s^.root, name);
+    RETURN (node # NIL) & (node^.kind = KindProc)
+  END MethodExists;
+
+PROCEDURE PushClassMembers (t: TypeIndex): BOOLEAN;
+(* Pushes a scope with t's fields (KindField) for method bodies in
+   CLASS IMPLEMENTATION. Method NAMES are deliberately not copied:
+   bodies re-enter them fresh via EnterProc (copying would collide
+   as duplicates); sibling-method visibility waits for step 4 calls,
+   when unknown names there honestly report 201. *)
+  VAR r: TypeIndex;
+    s: ScopePtr;
+    f: FieldPtr;
+  BEGIN
+    r := Resolve(t);
+    IF (r = InvalidType) OR (tform[r] # FClass) THEN RETURN FALSE END;
+    s := tscope[r];
+    IF s = NIL THEN RETURN FALSE END;
+    PushScope;
+    f := fields;
+    WHILE f # NIL DO
+      IF f^.owner = r THEN
+        IF Enter(f^.name, KindField) THEN
+          SetSymType(f^.name, f^.typ)
+        END
+      END;
+      f := f^.next
+    END;
+    RETURN TRUE
+  END PushClassMembers;
+
+PROCEDURE MarkVirtual;
+  BEGIN
+    IF curProc # NIL THEN curProc^.virt := TRUE END
+  END MarkVirtual;
+
 (* ---------------- procedures ---------------- *)
 
 PROCEDURE PushProc (node: SymPtr);
@@ -522,6 +604,7 @@ PROCEDURE EnterProc (name: ARRAY OF CHAR): BOOLEAN;
     node^.plink := NIL;
     node^.isVar := FALSE;
     node^.fwd := FALSE;
+    node^.virt := FALSE;
     curProc := node;
     curPTail := NIL;
     PushProc(node);

+ 7 - 0
compiler/tests/t_bad_parent.mod

@@ -0,0 +1,7 @@
+MODULE TBadParent;
+TYPE
+  CLASS Sub (Nope);
+    n : INTEGER;
+  END Sub;
+BEGIN
+END TBadParent.

+ 18 - 0
compiler/tests/t_class.mod

@@ -0,0 +1,18 @@
+MODULE TClass;
+TYPE
+  CLASS Base;
+    n : INTEGER;
+    PROCEDURE Set(v : INTEGER);
+  END Base;
+  CLASS Sub (Base);
+    PROCEDURE Get() : INTEGER;
+  END Sub;
+CLASS IMPLEMENTATION Sub;
+  PROCEDURE Get() : INTEGER;
+  BEGIN
+    RETURN 0
+  END Get;
+BEGIN
+END Sub;
+BEGIN
+END TClass.

+ 10 - 0
compiler/tests/t_classdef.mod

@@ -0,0 +1,10 @@
+DEFINITION MODULE TClassDef;
+TYPE
+  CLASS Shape;
+    x, y : INTEGER;
+    kind : INTEGER;
+    VIRTUAL PROCEDURE Draw;
+    PROCEDURE Move(dx, dy : INTEGER);
+    PROCEDURE Area() : INTEGER;
+  END Shape;
+END TClassDef.

+ 34 - 10
docs/OOP.txt

@@ -1,22 +1,46 @@
 ClassDecl ::=
-	CLASS Classidentifier['(' Name { ',' Name } ')'] ';'
+	CLASS Classidentifier ['(' Classidentifier { ',' Classidentifier } ')'] ';'
 		[ClassFieldDefList]
 		[MethodDecList]
 	END Classidentifier ';'
-	
+
 ClassFieldDefList ::=
 	ClassFieldDef {';' ClassFieldDef }
-	
+
 ClassFieldDef ::=
-	IdentifierList ':' TypeDef |
-	Identifier '=' Name
-	
+	IdentifierList ':' TypeIdentifier |
+	Identifier '=' Identifier
+
 MethodDecList ::=
-	MethodDec {',' MethodDec }
-	
+	MethodDec {';' MethodDec }
+
 MethodDec ::=
-	[VIRTUAL] PEOCEDURE MethodIdentifier
-	[FormalList [':' Name ]]
+	[VIRTUAL] PROCEDURE MethodIdentifier
+	['(' [FormalParams] ')' [':' TypeIdentifier]] ';'
+
+FormalParams ::=
+	[VAR] IdentifierList ':' TypeIdentifier { ';' [VAR] IdentifierList ':' TypeIdentifier }
+
+ClassImpl ::=
+	CLASS IMPLEMENTATION Classidentifier ';'
+		{MethodImpl ';'}
+		[BEGIN [StatementSeq] ]
+	END Classidentifier ';'
+
+MethodImpl ::=
+	[VIRTUAL] PROCEDURE MethodIdentifier
+	['(' [FormalParams] ')' [':' TypeIdentifier]] ';'
+	(FORWARD | Block Identifier)
+
+NOTES (V3 implementation, m2compiler-V3):
+- Method separators are ';' (as in the Table example below),
+  not ',' as earlier drafts of this sketch had it.
+- "PEOCEDURE" typo in the original sketch fixed to PROCEDURE.
+- Single inheritance only: extra parents are rejected (230).
+- Classic identifiers only (no '_'); the Table example's
+  _Find-style names are lexically out of reach for now.
+- Class lowering (vtable, dispatch, THIS) is deferred; the
+  construct parses, scopes and checks, ending in 230.
 	
 
 

+ 78 - 0
docs/summary_step1.10.md

@@ -0,0 +1,78 @@
+# V3 step 1.10 — Clarion classes: decl + inheritance + impl blocks (done 2026-09-19)
+
+Pre-step-3 grammar work from `docs/OOP.txt` (757-line Clarion CLASS
+spec + Table AVL example). Suite 33/33 (14 run + 19 reject).
+Grammar stays effectively LL(1)-clean (one benign Coco warning,
+see below).
+
+## Locked decisions (interactive §5)
+
+- Classic identifiers stay: no underscores, so Table's `_Find` etc.
+  remain lexically out of reach (noted, not solved).
+- Method separator `;` — the Table example overrules the sketch's `,`.
+- Construct now, lowering later (units/procs/nesting pattern).
+  Full `Table.mod` needs steps 3+4+stdlib (pointers, calls, SIZE…).
+- `CLASS IMPLEMENTATION` + single `(Parent)` inheritance included
+  now (parsed, scoped, 230); multiple parents → 230.
+
+## Grammar (`compiler/src/M2.atg`)
+
+- One `ClassItem = "CLASS" ("IMPLEMENTATION" … | …)` in both the
+  `TYPE` loop and `DeclSeq`: splitting decl/impl across the two
+  loops needs 2-token lookahead (`CLASS ident` vs
+  `CLASS IMPLEMENTATION`), which LL(1) cannot do.
+- `ClassRest`: name + optional parents + `;`, `{field ";"}`,
+  `{method ";" + CloseProc}`, `END`-match, one 230. Fields reuse
+  the record machinery (`FieldPending/FixPendingF`); `x = Name`
+  constant form included.
+- `Parents`: first parent resolved (201 unknown, 230 non-class,
+  stored via `SetParent`); extras consumed + 230 (single
+  inheritance locked).
+- `MethodHeading = [VIRTUAL] ProcHeading` (+ `MarkVirtual`);
+  bodies reuse `Block` (nested procedures inside methods work,
+  as in Table's `Search`/`_Find`); impl-side existence check
+  (`MethodExists`, else 201); `FORWARD` allowed in both.
+- `ClassImplRest`: class resolution (201/230), `PushClassMembers`,
+  methods, optional `BEGIN` body, `END`-match, one 230.
+- Field *use* (`KindField` in Design) is now explicit 230 instead
+  of silent `0` — no silent acceptance anywhere anymore.
+
+## SymTab (additive)
+
+`FClass`/`ClClass` (=11); `NewClass`, `SetParent`, `PushClassScope`
+(records the body scope on the descriptor), `MethodExists`,
+`PushClassMembers` (fields only — see bug 1), `MarkVirtual`
+(`virt` node flag); `FieldPending`/`PushRecord` accept `FClass`;
+methods stay `KindProc` (class membership rides `node^.scope`).
+
+## Bugs caught
+
+1. **Member-copy collision**: first cut re-entered sibling methods
+   into impl member scopes, so every method body heading hit
+   `200 duplicate` (+ `232` cascade via cleared `curProc`).
+   Fix: members push fields only; sibling calls honestly 201
+   until step 4.
+2. **Coco's lingering TypeBlock warning** (`CLASS` start+successor):
+   verified benign — the generated `WHILE`/`IF` decision is correct
+   by inspection and by behavior on every input shape. Coco is
+   strict about `FIRST ∩ FOLLOW` on that loop; the code it emits
+   does the right thing. Documented, not "fixed".
+3. **A phantom `t_class` failure** (`ident expected` at the impl
+   line) appeared during development across many consistent runs,
+   then vanished and never reproduced (deterministic 3/3 + full
+   suite since, including from a clean rebuild). Token-stream dumps
+   of failing vs working inputs were byte-identical through the
+   failure point. No mechanism found; no code changed to "fix" it.
+   If it ever recurs: clean-rebuild first, then suspect toolchain
+   (CR codegen) rather than the grammar — the decision code reads
+   correctly. Recorded here so nobody re-bisects it blind.
+
+## Tests
+
+`t_classdef` (fields, `VIRTUAL`, empty/full formals, result →
+2× 230: class + unit), `t_class` (inheritance + impl + body with
+`RETURN 0` → 4× 230, zero cascades), `t_bad_parent` (unknown
+parent → 201 + 230). Total 33 (14 run + 19 reject).
+
+Deferred: vtable/dispatch/`THIS`, multi-inheritance, opaque
+classes, `SHORTINT [-1..1]` postfix bounds, full Table (steps 3+4).