← board

A bare method name in argument position is called instead of referenced

Repro — 14 lines, no generics

program p; {$mode delphi}
type
  TOnHash = function(const v: LongInt): LongInt of object;
  TC = class
    function HashIt(const v: LongInt): LongInt;
    procedure Take(const h: TOnHash);
    procedure Go;
  end;
function TC.HashIt(const v: LongInt): LongInt; begin Result := v + 1; end;
procedure TC.Take(const h: TOnHash); begin WriteLn('took ', h(41)); end;
procedure TC.Go; begin Take(HashIt); end;      { <-- here }
var c: TC;
begin c := TC.Create; c.Go; end.

fpc 3.2.2 -Mdelphi: took 42. pxx: no overload of Take matches these arguments / argument types: (LongInt).

The dialect is load-bearing. In objfpc a bare method name IS a call and @ is required — fpc rejects the same program there, with a different message. A probe written in objfpc mode says nothing about this bug; the corpus unit is {$MODE DELPHI}.

The matrix — which contexts already work

context pxx fpc
F := HashIt (assignment) ok ok
Result := HashIt (return) ok ok
Take(HashIt) (argument) refusedno overload of Take matches ok
Take(Self.HashIt) (qualified argument) refusedwrong number of parameters ok

Two diagnostics, one missing reading — which is why it can read as two bugs.

Mechanism

TryParseParenlessMethodRef (pasparser_call.inc) is the ONE place an AN_METHODREF is built for this family, and its own comment says to count construction sites rather than add to them. It has callers for a method-pointer CAST and for the assignment site. Argument position has a different door, TryDelphiBareProcArg (pasparser_lval.inc), and that door asks FindProc — which does not see a method. So a bare method name in an argument list never reaches the reference reading at all and falls to ParseArgExpr, which reads a call. A rule spelled per CALLER, failing by an absent copy.

What was tried, measured, and REVERTED

Routing the method case into the one helper from TryDelphiBareProcArg:

Two facts any future attempt needs, both measured:

Why the precedence cannot simply be the assignment site's

The assignment site knows its DESTINATION and can ask MethodResultSatisfiesTarget — would calling this already produce the method pointer the target wants? In argument position the destination is not known yet, because overload resolution has not run. The question available there is the free-routine one: can this be called parenless at all? A paramless function can, and Delphi calls it; a routine that REQUIRES arguments cannot, so the reference is the only reading that compiles.

2026-09-09 — the parse half is SOLVED and it is not enough (frankuser)

Attempted, measured, not landed. TryDelphiBareProcArg in compiler/pasparser_lval.inc asks FindProc, which sees free routines only, so inside a method body the reference reading was never reached at all. Adding a method arm — mirroring TryParseParenlessMethodRef's implicit-Self arm in pasparser_call.inc verbatim, ASTRight := UMthVirSlot included — makes the repro COMPILE. It then segfaults at the indirect call:

ok: bare  [code=69400B data=3504B bss=43532B procs=140]
took            <- prints the literal, dies calling h(41)
Segmentation fault (core dumped)

So the ticket's own warning holds and this is the same wall the earlier attempt hit: the parser is now right and something downstream is not. What that changes is the search area — it is no longer 'find the third site that decides the reading'. The reading is decided correctly; the defect is in lowering the AN_METHODREF argument to a method-pointer temp, or in the temp's layout at the call. ir.inc:5659 already routes AN_METHODREF to IRMethodRefToTemp when the param is tyRecord, so the route exists and the value it produces is wrong — check the Self operand actually reaches the temp, and check the two-word method-pointer layout against what the of object call site reads.

Do not land the parser arm alone. Today's no overload of Take matches is an honest refusal; a segfault is not, and the arm converts one into the other.

Three measured facts worth keeping, all cheap to get wrong:

The working arm is reproducible from this description in about ten minutes; it was deliberately not committed, because a patch that turns a diagnostic into a crash is worse to inherit than a description of one.

Resolved 2026-09-09 by frankH — the loop never offered the door; the lowering was never wrong

took 42, matching fpc 3.2.2 -Mdelphi byte for byte. Two changes, and neither is in ir.inc.

The diagnosis above pointed one layer too deep, and the AST says so

The handoff's reading was that ir.inc:5659 routes AN_METHODREF to IRMethodRefToTemp "and the value it produces is wrong". That route never ran. With the parser arm in place and the repro compiling, PXXDBG=a.ast:* gives the argument as

#8202 kind=8 tk=11 ival=137     { AN_CALL of TC.HashIt, tyInt32 }
  #8200 kind=9
    #8201 kind=3 tk=6 ival=105  { AN_IDENT Self }

kind=8 is AN_CALL and tk=11 is tyInt32 — not AN_METHODREF/tyRecord. ir.inc:5659 tests exactly those two things, so it could not fire, and the IR confirms it: IRMethodRefCode emits IR_PROCADDR (non-virtual) or load_mem/binop/load_mem (virtual) and neither emits a call, while TC.Go contained call a=137 with Self as its only argument, its LongInt result stored into the 16-byte temp and then invoked as a code address. IRMethodRefToTemp and its route were correct throughout.

The actual defect: one arm of a double door

pasparser_stmt.inc's bare-Self method-call loop asked TryParseBracketArgForSlot and not TryDelphiBareProcArg. The free-proc loops at pasparser_stmt.inc:8550 and pasparser_expr.inc:8525 ask both, side by side. So the reference reading was never offered here at all and ParseExpr took the bare name under call-first precedence.

That loop's own comment already records this shape, about the OTHER door: "Every other call path asks; this one — the bare Log(...) inside a sibling method, implicit Self — hand-rolled its loop and asked nothing." The fix for that added one of the two doors the other paths ask at. This is normalise-dont-special-case.md's "fixed one arm of a double case? Grep for the sibling before closing", with the sibling in the same five lines.

Why it presented as a lowering bug, which is the transferable part

TryDelphiBareProcArg is reached — from the overload probes, which parse the argument speculatively. Instrumented, the new method arm fires twice, building a correct AN_METHODREF each time, and both are discarded with the probe. Those firings are what made Take match. This loop then re-parsed the argument as a call.

So the verdict came from one reading and the tree from another. That is precisely why the symptom looked downstream: the overload match had already succeeded on a methodref, so the argument "was" a methodref by the time anyone asked, and the only wrong thing left to suspect was the lowering. The discriminator was dumping the AST rather than reasoning from the match.

The two changes

  1. TryDelphiBareProcArg (pasparser_lval.inc) gains a method arm for when FindProc finds nothing: resolve the implicit Self exactly as TryParseParenlessMethodRef's no-receiver arm does and build its node.
  2. That loop (pasparser_stmt.inc) asks the door.

Both of the handoff's warnings were load-bearing and are now under test rather than under comment:

FindUMethOverloadAhead's hookless ParseArgExpr probe is still a real second gap and is still not this bug's cause, exactly as the handoff said. It is why the arm fires twice; it is not why the tree was wrong.

Gate GREEN (20 PASS). test_delphi_bare_method_name_in_argument_position is wired beside the chained-receiver test; .expected is fpc's own output.

Log