← board

The Self shift forgets puntyped, so a method parameter is mislabelled

pnames, ptypes, pconst, pout, pdefault*, pDynDepth, pDim* all shift when Self is injected at slot 0. puntyped did not, at any of the three sites: two in pasparser_proc.inc (the method-impl shift and its __genself twin) and one in pasparser_decl.inc.

IsUntypedVarParamSym (pasparser_stmt.inc) matches the parameter NAME at slot i against the FLAG at slot i. Moving one and not the other therefore mislabels both ends at once — which is why one omission produced two opposite defects.

Measured, and both halves pre-date the fix

Byte-identical parameter lists, free routine vs method. Pin fe1e9c37d322 and HEAD agreed, so this is not a recent regression:

construct free routine method correct
Integer(b) := 99, b untyped accepted REFUSED accepted
Int64(a) := 99, a a 4-byte Integer refused ACCEPTED refused

The cast-as-lvalue arm is legal on an untyped parameter precisely because such a parameter has no declared width for the cast to disagree with. The second row is the dangerous direction: an 8-byte store into a 4-byte slot, accepted with no diagnostic.

How it stayed invisible

Nothing asked a method parameter whether it was untyped. The construct that does — the cast-as-lvalue arm — is rare, and the overload-matching readers of ProcParamUntyped are reached for methods only on a path that, at HEAD, does not type-check single-candidate method arguments at all.

A probe I built for this first could not fail, and the controls caught it: an interface method binding a string to an Integer parameter was accepted — but so was the same call with no untyped parameter anywhere, so the probe was measuring the missing check, not the shift. The discriminating pair had to be the two cast-as-lvalue directions, where free and method spellings differ.

Fix

puntyped[i] := puntyped[i-1] in both pasparser_proc.inc shift loops and mPUntyped in the pasparser_decl.inc one, plus [0] := False beside the existing pconst[0] := False (Self always has a type).

test/test_method_untyped_param_self_shift.pas, wired into test-core, asserts that the method matches the byte-identical free routine and that both give the right value — an equality-only check would pass on two identical zeros. It carries no per-target constant. Positive control: the pinned compiler refuses to compile it, at the method, cast-as-lvalue: the cast type must be the same size as the variable.

Found while diagnosing the single-candidate gate widening in refactor-p-the-overload-probe-still-cannot-answer-two-argument-shapes: two of that experiment's regressions were this bug becoming visible once the gate started reading ProcParamUntyped for methods.


The concept-driven audit, and the four it found that a caller census could not

puntyped was found by tripping over it. That is not a method, so afterwards I asked the question the other way round: which arrays are indexed by parameter slot? — enumerate every declared array[0..MAX_PROC_PARAMS-1] local and difference it against what each shift loop actually carries.

per-param arrays declared            37
site A (method impl) shifts          33   missing: pFixedLen pFixedLo
                                              ptypesFileRecSize ptypesFileElemTk
site B (__genself)   shifts          25   ...plus pdefault* (7) and ptypesStrCap

A census of any one array's CALLERS cannot find these, and I nearly ran one: it returns only the sites that already reached the helper, and the defect is a site that never did. The population has to come from the concept, never from a call graph. (frankD reached the same conclusion the same night from the opposite direction — a tidy "three sites, all covered" that was wrong by two.)

Both new rows measured, on the pin

pFixedLen / pFixedLo — a named fixed-array parameter's element count and low bound. For W(n: Integer; var arr: array[1..3] of Integer) assigning arr[1..3]:

result
free routine 10 20 30
fpc 3.2.2, both spellings 10 20 30
method 0 10 20and the 30 landed past the end of the array

The low bound lands on the Integer slot and the array slot reads an unset 0, so every index is off by lo. A silent out-of-bounds write, no diagnostic.

ptypesFileRecSize / ptypesFileElemTk — a file of T parameter's element width and kind. One file parameter shows nothing: both arms answer FileSize = 1, because the expected value collides with the answer a mis-sized slot happens to give. Two file parameters of different widths discriminate, and then the method body fails to compile outright:

pascal26:22: error: read/write(file): the variable is 4 bytes
                    and the file's element type is 20

— the first file parameter wearing the second's record size. Fail-closed here, unlike the array row.

The __genself site was a sibling arm of an already-fixed bug

Site B never shifted pdefault* at all, though it clears pdefault[0]. The twin loop's own comment records what that cost when it was missing there: "a method impl that repeats its defaults silently gave M(a: Integer = 1; b: Integer = 2) a=2 on a defaulted call"bug-pascal-method-default-param-self-shift. That fix landed on one arm and the sibling kept the defect, which is the shape normalise-dont-special-case.md names.

Verification

Census 1874 files, 1 row differs and it is the new test file itself. Conformance 384 pass / 1 fail, unchanged. fgl 7/7. test/test_method_param_self_shift_family.pas asserts both new rows as relations against the byte-identical free routine, so it carries no per-target width; the pin refuses to compile it.