← board

class helper for <class> is not accepted

Found 2026-08-22 by an FPC differential sweep over language shapes (fpc -Mobjfpc -O1 3.2.2 vs pxx f73eca492).

Repro

type
  TC = class
    x: Integer;
  end;
  TCH = class helper for TC
    function Twice: Integer;
  end;
function TCH.Twice: Integer; begin Result := x * 2; end;
...
  c := TC.Create; c.x := 21; Writeln(c.Twice);

fpc prints 42. pxx:

Expected: :, but got: for (Kind: 20, Line: 6)
pascal26:6: error: unexpected token

Why this is small, and where it goes

The concept is already implemented twice in pasparser_decl.inc: record helper for <type> (~line 5005) and type helper for <type> (~line 5022, FPC's alternate spelling), both building a helper-marked class-like entry with UClsHelperTk / UClsHelperRec and dispatching methods on the target with Self as parameter 0. class helper is the same shape with two differences worth getting right rather than guessing:

Per normalise-dont-special-case.md, add the arm to the SAME machinery rather than a third parser path, and check the two existing spellings still agree afterwards.

Gate

make compiler/pascal26 + a test covering a class helper reading and writing the target's fields, a helper on an ancestor reached through a descendant, and the record/type helper rows kept as controls + tools/gate.sh quick.

Outcome (2026-08-27) — DUPLICATE, closed by the same change

This is the same ticket as compat-pascal-class-helpers (opened 2026-08-05, found by the probe; this one 2026-08-22, found by the differential sweep). Both were closed by one change; see that ticket's Outcome for the design.

Its two predictions were both right and both load-bearing: Self is the object pointer BY VALUE, not the target by reference; and the lookup has to resolve through the class's ANCESTRY, not an exact match. The ancestry part turned out to need more than a widened match — the walk must INTERLEAVE, asking at each class for that class's helper before that class's own members, or a descendant's own override loses to a base-class helper (FPC answers d.Name = the override and TBase(o).Name = the helper, on the same object).

The gate this ticket asked for is what shipped: test/test_class_helper_for_a_class.pas covers reading AND writing the target's fields (rows i/i2, unqualified and through Self), a helper on an ancestor reached through a descendant (row g), and the record/type helper rows stay in test-core beside it as controls.

Log