summary_class-lowering.md 4.2 KB

Step: Clarion CLASS lowering

Tag v3-class-lowering. Suite 141/141; fixpoint OK (image 2,188,065 bytes).

What "CLASS lowering" means

Clarion (a 1990s PC Modula-2 dialect) added OOP to classic Modula-2 — the syntax is preserved in docs/OOP.txt. "Lowering" is translating a high-level construct into what the backend already understands. Previously V3 parsed and checked classes but ended every one in error 230 and emitted no method bodies ("declared + checked only"). This step makes non-virtual classes fully executable:

TYPE
  CLASS Point;
    x, y : INTEGER;
    PROCEDURE Set(a, b : INTEGER);
    PROCEDURE Sum() : INTEGER;
  END Point;
CLASS IMPLEMENTATION Point;
  PROCEDURE Set(a, b : INTEGER); BEGIN x := a; y := b END Set;
  PROCEDURE Sum() : INTEGER; BEGIN RETURN x + y END Sum;
END Point;
VAR p : Point;
BEGIN p.Set(3, 4); ExitCode := p.Sum() END   (* -> 7 *)

How it works

  1. Layout — a class is a record: ComputeOffsets gives fields declaration-order offsets. Single inheritance lays the child's own fields after the parent's (baseOff), and FindField / FieldExists / FieldOffset walk up the parent chain, so obj.field works through the existing record path.
  2. Receiver (THIS) — each method gets a hidden first VAR parameter THIS of the class type. At the top of the body it is pushed as the current WITH base, so a bare field name resolves to THIS^.field — reusing all the WITH addressing machinery. (SymTab.EnterThisParam, QbeGen.ThisBase/PushWith.)
  3. Calls — obj.M(args) binds statically: Design recognises a class method, arms the receiver (QbeGen.ArmRecv), and ArgList emits the call with obj's address as the hidden first argument (CallBegin prepends the armed receiver). Formal types come from ClassMethodParamType/…IsVar (user index 0 skips THIS).
  4. Implementation binding — ResumeMethod reuses the method symbol declared in the class scope (so the call-site uid matches the definition's, and MethUid reports it since the class scope is detached by then).
  5. VIRTUAL is accepted and dispatched statically for now (no vtable yet).

Supported / not yet

  • ✅ Fields (obj.field, WITH obj, inherited fields), methods (value + VAR formals, value-returning), single inheritance (inherited fields and methods; a child method sees both), classes in a DEFINITION/IMPLEMENTATION pair.
  • ⏸ Dispatch for VIRTUAL (currently static).
  • ⏸ Bare sibling-method calls inside a method body (Add(a) rather than obj.Add(a)); call through an object.
  • ⏸ A class's BEGIN … END initialisation body (still 230).
  • ⏸ Multiple parents (still 230, as designed).

Implementation notes

  • V3 is single-pass (declaration before use), so the class helpers had to be ordered: Resolve/ParentOf/PushProc early, LayoutClass after ComputeOffsets, EnterThisParam after EnterParam. (gm2 tolerates forward references; the fixpoint's V3 stage is the guard.)
  • MethodImpl now emits the body: EndFuncHeader (was missing, so bodies leaked to top level), the receiver binding, Block, then PopWith/CloseProc/EndFunc.
  • EnterThisParam clears the pending-formal buffer so THIS isn't emitted twice by ParamSection.

Tests

  • t_classmethod.mod (7): fields via the receiver, value + VAR formals, value-returning method, VIRTUAL static dispatch.
  • t_classinherit.mod (7): inherited method + inherited field seen from a child method.
  • TClassDefProg (8): a class declared in a DEFINITION used as data.
  • t_class.mod still rejects (its BEGIN init body is 230).

Files

compiler/src/SymTab.def/.mod (parent field inheritance, ParentOf/EnterFieldsOf, EnterThisParam, ResumeMethod, MethUid, ClassMethod*, LayoutClass), compiler/src/QbeGen.def/.mod (ThisBase, ArmRecv, receiver argument in CallBegin), compiler/src/M2.atg (ClassRest/ ClassImplRest/MethodImpl, ProcHeading receiver + method resume, Design method branch, ActParam/ArgList method formals), compiler/tests/{t_classmethod,t_classinherit,t_classdef_prog}.mod, docs/features.md.