-
Notifications
You must be signed in to change notification settings - Fork 0
Attribute store through a Protocol-typed receiver bypasses the setter check and panics in pycc_mir #958
Description
Summary
An attribute store through a Protocol-typed receiver is not checked against the conforming class's write surface. When the conforming class satisfies the protocol's attribute member with a read-only @property, pycc_types::check accepts the program and pycc_mir panics with an internal error instead of emitting a diagnostic.
Found by the iteration-18 adversarial advisor round while sizing #914 (a class attribute satisfying a protocol attribute member); #914's fix would add a second route into the same hole (a read-only class attribute reached through a protocol-typed receiver), so this defect is recorded separately rather than folded into #914.
Reproduction (observed on main = 73b15320, debug build, 2026年09月06日)
from typing import Protocol class HasLimit(Protocol): limit: int class Impl: @property def limit(self) -> int: return 1 def bump(p: HasLimit) -> None: p.limit = 2 i = Impl() bump(i) print(i.limit)
pycc build repro.py:
thread 'main' panicked at crates/pycc_mir/src/stmt.rs:582:25:
pycc_mir: internal error: property `limit` on class `Impl` has no setter -- pycc_types::check should have rejected this assignment before it reached pycc_mir
Expected: a compile-time diagnostic. CPython raises AttributeError: property 'limit' of 'Impl' object has no setter at runtime; pycc's convention (a wrong program is rejected at check time) points at a T0044-family rejection at the p.limit = 2 site after monomorphization, or at the call site (bump(i)) if the check is kept on the conformance side.
The same program with an instance attribute (self.limit = 1 in __init__) compiles and prints 2, which is correct and must keep working.
Root cause (as read on 73b15320)
check_attr_set(crates/pycc_types/src/class.rs:768) gates its class-attribute rejection and its property-setter walk onTy::Instance; aTy::Protocolbase falls through toresolve_attr_get, which only answers "does the member exist with this type".monomorphize_protocol_params(crates/pycc_types/src/monomorphize.rs, D-166) specializes the function body for the concrete class but performs no re-check of attribute stores against the concrete class's property/class-attribute tables.
Scope
- Decide where the write check lives: on the protocol side (a protocol attribute member is read-write; a class whose member is a setter-less property or, after A class attribute should satisfy a
Protocolattribute member #914 , a class attribute does not conform when the protocol member is written through -- mypy/pyright do not track this) or on the monomorphized body (re-runcheck_attr_setagainst the concrete receiver class after specialization). Record the choice indocs/TYPE_SYSTEM.md. - Cover: setter-less property (panic above), property with setter (must keep working), instance attribute (must keep working), and -- once A class attribute should satisfy a
Protocolattribute member #914 lands -- read-only class attribute. - 100% line and region coverage (D-014).
References
- A class attribute should satisfy a
Protocolattribute member #914 (class attribute satisfies a protocol attribute member) -- adjacent; do not expand its scope with this. - Calling a protocol member with a protocol-typed parameter rejects a conforming concrete argument with T0021 #953 / D-166 (protocol parameters monomorphized).
- Part 1 of #885: ClassVar registration and annotated scalar class attributes #911 (class attributes are rejected on every write path).