Type a bare parameter as a class from its call sites
The second lever on top of the import closure (feature-n-lex-the-import-
closure-...). The demo's hot methods take objects: Quat.rotate(self, v),
Vec3.dot(self, o), inverse_rotate(self, v) all gave up at the first site
in the closure census because the argument was a Vec3 -- a class, which the
scalar typer could not claim. Now it can.
Mechanism. In PyParamTypeFromSites, a site's argument names a class
when the expression typer answers tyClass with a class index, when the
caller's own parameter was typed as a class by ITS sites (the existing
recursion, reading PyPSLastCi), or when the argument is exactly self,
exactly Cls(...) / mod.Cls(...), or a local of the enclosing def bound
once to a construction -- read by PyStaticReceiverClass, the same reader
that attributes a .name( site by its receiver, given the argument's end
where the . would sit. An expression that merely ENDS in a construction
(a + Vec3(...)) is whatever the operator returns and is not read. Every
site must name the same class; a subclass at one site and its base at
another is a disagreement. A = None default vetoes (a class slot holds no
None), and so does any untypeable site, even beside a class default (a
variant reaching a class slot is not a loud coercion). The class must be
registered when the answer is computed (FindUClassNonRecord; a class in a
module the parser has not reached is a name only), and the body must not
rebind the parameter at all (PyBodyBindsName: v = v.norm() or v = None would store something else into a class slot). The answer is memoised
with its class index (PyPSMemoCi), so the shell, the pre-pass and the body
parse agree. Consumers: the def header and PyParseMethod set the param's
class index from PyPSLastCi; the constructor field typer gives the field
REC_UCLASS_BASE + ci. The class pre-pass registers the parameter as
tyClass without a rec id, exactly as an annotated class parameter is.
What it does to the earlier fixture: tick(g, dt, n) in
test_nilpy_a_bare_parameter_is_typed_from_its_call_sites now types g as a
Grid (tk=6 cls=Grid), values unchanged.
Residuals, owner this seat: a class declared LATER in the same module
than the def whose parameter it would type is not registered yet at the
pre-pass and the (memoised) answer is variant; a module-level constant as an
argument (v.step(SIM_DT)) is untypeable -- the local typer knows only the
def's locals -- and that is the next lever (Vessel.step's dt in the demo
gives up on exactly that); an element of a list (for item in rows: item.span) is untypeable.
Log
- 2026-09-16 frankuser (Fable): built on a scratch binary while the closure's tier ran; fixture green, psites and closure fixtures green, commit 1a0b42411.
- 2026-09-16 frankuser (Fable): the full tier on 1a0b42411 was RED on test_nilpy_subscript_dunder_spellings:
get({'a': 1}, 'a')-- PyEndOfGroup did not count{/}, the dict literal ran to the end of the call, the comma inside it split the call into the wrong positionals, the ARITY FILTER dropped the site, and the str it carried never vetoed the int claim from the other sites. A wrong claim shipped for one tier; fixed by counting braces. Lesson for the filter: a site the splitter misreads is dropped silently, and a dropped site is the one way attribution lets a wrong claim through (the doc comment said so; the tier proved it). Two more found by a probe the same hour:v.dot(self)was skipped as an unboundV.dot(because FindUClass is case-insensitive (now PyClassDeclTok only, exact case); andself.<field>in a site's argument was untypeable because the typer's self arm reads the parse-time CurSelfClass only -- the site loop now lends the enclosing method's class (PyInferSelfCi) and the typer's field-chain arm accepts it. What the probe ALSO showed, banked here: a field of a class registered LATER than the def whose parameter it would type (B'sself.ufeeding V.dot'so, and in the demo sim.py'sself.torquefeeding math3d's Quat.rotate) is still unknown at the memoised first ask; the closure makes every SITE visible, but class registration is still one module at a time, so the field lever needs a class pre-pass over the whole closure before any module's methods are typed. That is the lekkerzeilen seat's "one round short" finding, confirmed from the compiler side.