← board

collections.Counter is unreachable through its qualified spelling

Measured 2026-09-09 (compiler 418064fca1d3)

spelling result
Counter() (no import at all) works
from collections import Counter then Counter() works
import collections then collections.Counter() refused

The refusal reads no member Counter came of the qualifier collections — check what collections resolves to; an import that bound nothing gives exactly this, which is accurate and points at the right half of the line.

Why the qualified spelling is the one that matters

Every use site in the lekkerzeilen corpus is module.name; not one is from module import name. The two spellings that work are the ones nobody writes. That is what made the sibling deque gap worth fixing, and it applies here identically — the difference is only that nothing measured reaches collections.Counter yet, which is why this is prio 35 and deque was not.

Why it was not fixed alongside deque, stated so nobody "fixes" it wrongly

collections.deque is now mapped in PyStdlibCallProc (compiler/pyparser.inc), the same table that carries math.pow -> Power. That table is consulted before unit-member lookup — measured with math.pow, which resolves through it while lib/rtl/math.pas exists and is loaded — so it needed no resolver-order change and left collections.abc untouched.

It cannot carry Counter. PyParseStdlibCall resolves its target with FindProc(pname) plus a RE-TARGET BY ARITY step, and its own comment says "Type-based selection is still not reachable this way". Counter is:

function Counter: TPyDict;
function Counter(l: TPyList): TPyDict; overload;
function Counter(const s: AnsiString): TPyDict; overload;

The two 1-argument overloads differ only by type. An entry in that table would make collections.Counter(some_string) and collections.Counter(some_list) compile to the same body — a silently wrong count, where today there is a loud refusal. That trade is the wrong direction and this ticket exists to record that it was considered and declined, not overlooked.

Also declined: adding deque's trick of a distinct internal name (pydeque_new). It removes the SHADOWING hazard, not the overload one.

Routes that would actually close it

  1. Type-aware selection in the stdlib-call table (helps every future entry).
  2. A qualified-member fallback: when a qualifier resolves to a unit that has no such member, and the root is a known stdlib module name, retry the member as a bare pylib symbol. Wider, and it would also fix itertools.count(), which fails differently today (undefined variable (itertools)) for the same underlying reason — a qualified stdlib member has no single resolution path.

A FOURTH MECHANISM EXISTS AND MAY BE THE RIGHT ONE — measure it before choosing a route

Found while landing deque, recorded here because this ticket is where it pays.

ResolveUsesUnitSource (compiler/pasparser_proc.inc, ~5320) already refuses a genuinely-Pascal unit for a BARE NilPy import — "a bare import means Python, so a genuinely-Pascal unit is refused BY NAME rather than bound and failed one token later" — and, crucially, it checks PyMimicShimExists first:

After the .py/.npy and host-header probes and before the shim mapping: a mimic_ shim IS the Python module of that name, so if one exists it answers and there is no collision to report.

So the resolver may ALREADY prefer a shim over a same-named Pascal unit, which would mean lib/rtl/mimic_collections.py re-exporting Counter, deque and the rest is a complete fix for the whole module in one file, with no table entry and no per-name work — and it would fix collections.Counter for free.

This is a reading, not a measurement. It sits against the comment at PyImportRootPlainIsConsumedOnly, which says collections is deliberately left out of the plain-import consume list "so a collections.Sym qualifier has a unit to resolve against" — and that unit is lib/rtl/collections.pas, which can never carry a Python name, so at least the STATED REASON there is false. What is not established is which of the two the resolver actually does today, because the failing diagnostic cannot tell them apart: the error site says so in its own comment — "this site cannot tell a receiver that resolved to nothing from one that resolved and has no such member".

The measurement that settles it is one file: write lib/rtl/mimic_collections.py exporting one probe name, and see whether import collections; collections.probe() reaches it. If it does, prefer that route over adding entries to the stdlib-call table, and consider retiring the collections.deque entry with it.