Skip to content

core: __str__ and __repr__ both map to ToString — a __str__-only class repr()s as its str (python: <__main__.C object at …>) #2057

Open

Description

@antonsynd

Cell: (program shape transcribed from the plan's measurement; re-run before fixing) __str__ and __repr__ both map to ToString (Shared/DunderNameMapping.cs:19-20; declaring both is SPY0522). So a user class that declares only __str__ reprs as its str:

class C:
    def __str__(self) -> str:
        return "s"
print(repr(C()))   # Sharpy: s    python: <__main__.C object at 0x…>

plan-0ca7b7 adds a Core IRepr channel for Core/Stdlib types (Optional, Template, datetime, string enums). Widening the user __repr__ dunder onto IRepr (so __str__/__repr__ are distinct members) is the feature; recorded as a deviation until then.

Found while planning plan-0ca7b7 (chores + P22 + P11e + P11f + P14b + P14c); measured @ acd1d40 with the built sharpyc. Filed as a sibling, NOT folded into that plan.

Activity

  1. changed the title [-]core: str() of a CLR IFormattable (System.DateTime) is current-culture text while f"{dt}" becomes invariant after #2031 — pick one[/-] [+]core: __str__ and __repr__ both map to ToString — a __str__-only class repr()s as its str (python: <__main__.C object at …>)[/+] on Sep 25, 2026
  2. antonsynd commented on Sep 30, 2026

    @antonsynd
    OwnerAuthor

    Batching 2026-09-30 @ db8e05b — charter .claude/plans/remediation-round-2026-09-20-batching.md §3.13, P11g — python-name/repr channel residue (order N5; lands alone; generator FIRST).

    Ruling R-CP (owner, 2026-09-30): user __repr__ synthesizes IRepr and __str__ stays ToString — distinct members; a __str__-only class reprs as <mod.C object> (R-CF); the str-only-class-repr-is-str ledger row retires with the fix. Rejected: keep as a deviation. Output change for repr of __str__-only classes — the plan counts fixture hits. Contract (as P11f): every runtime spelling of a value or type — str, repr, type(x).__name__, __name__ on a reference, every message naming a type — comes from the ONE python-name/repr authority (PyFormat.PyTypeName/PyQualifiedName, IRepr, [SharpyName]). Generator FIRST: the reflective #1995 str/repr sweep becomes a python3 DIFFERENTIAL over every public Core/Stdlib type × site, allowlist ratchet; hand rows only for the user-class/dataclass/exception/function cells reflection cannot instantiate. Lands alone (printed output and, under R-DG, .expected.cs move). Lane / reach: silent-wrong · program. Class tracker: #2104 (Stdlib repr family); this issue is the user-__repr__ tracker. No seam hypothesis — the plan measures the seam.

  3. added
    lane:silent-wrongCompiles and runs with wrong output or semantics (rank first)
    reach:programAn ordinary program a Python programmer would write hits it
    class-trackerThe one issue per class: carries the contract and the cell table
    on Sep 30, 2026
  4. antonsynd commented on Oct 16, 2026

    @antonsynd
    OwnerAuthor

    Cell (2026-10-15, found by P63's iterator lane repair, #2288; lane silent-wrong, reach program). print(c) / str(c) on a user class that defines __iter__/__next__ but no __str__ formats the object by enumerating it. The formatting side effect consumes the iterator, so the program's later behaviour changes.

    class Counter:
        index: int
        def __init__(self):
            self.index = 0
        def __iter__(self) -> Self:
            return self
        def __next__(self) -> str:
            if self.index >= 2:
                raise StopIteration()
            self.index += 1
            return "item"
    
    def main():
        c = Counter()
        print(c)
        print(c)
        n = 0
        for v in c:
            n += 1
        print(n)
    output
    sharpyc run (built CLI @ f8c37f9) ['item', 'item'] / [] / 0
    python3 <__main__.Counter object at …> ×2 / 2

    The repr TEXT is a deviation-by-default surface (python-fidelity-scope: message/repr text beyond the type name). The side effect is not: after print(c) the loop sees 0 items instead of 2, and with a __next__ that never stops, print(c) hangs. Contract: formatting an object is side-effect free and never iterates it. I'm posting it here as the nearest repr-channel tracker (__str__/__repr__ → ToString). It may be its own class (the default formatting of a user IEnumerable walks it) — for /batch-issues to place.

  5. antonsynd commented on Oct 21, 2026

    @antonsynd
    OwnerAuthor

    Batching 2026-10-20 @ a473af6 — charter .claude/plans/remediation-round-2026-09-20-batching.md §3.0 (and §3.17), chore in the current band (one commit, no plan); this issue stays the user-__repr__ tracker under P11g (order 17). Ruling R-HO (owner, 2026-10-20): D1 a — a Core chore in the current band: a Sharpy-compiled type is never a "plain CLR sequence" in Str and Repr.

    Measured @ a473af6 (built CLI) — the 2026-10-15 cell plus its sibling cells:

    dunders on the class str repr f-string hole list element loop afterwards
    __iter__/__next__ only ['item', 'item'] [] [] [[]] 0
    + __str__ Counter! Counter! Counter! [Counter!] 2
    + __repr__ Counter() Counter() Counter() [Counter()] 2
    none (plain class) <__main__.Plain object> same same [<…>] —
    __len__ + __getitem__ <__main__.Seq object> same same [<…>] —

    python3: <__main__.Counter object at …> on every site of row 1, loop 2. Row 2's repr is the R-CP cell already on this tracker. Rows 4–5 are the positive controls. The surface is COMPARED already (python-fidelity-scope row "user classes without __str__", R-CF) — no promotion needed; the side effect (iterator consumed; an endless __next__ hangs print) is the class.

    Internals (read @ a473af6): the emitter makes the class IEnumerator<string>, IEnumerable<string>; Repr.cs runs TryFormatPlainClrSequence before PyFormat.TryFormatInstanceFallback, and Str carries the same arm in the same order (#1453). Cure: the Sharpy-instance test precedes the plain-sequence arm in BOTH (one predicate, two callers — a parallel-site shape, so the matrix covers both); the table above as a Core.Tests theory + one run fixture asserting the loop count; mutation (swap the arms back → the row-1 cells red); direction record (['item', 'item'] / 0 → <__main__.Counter object> / 2); fixture-hit count FIRST — the chore rides P21e's gate if no .expected moves, its own gate otherwise. Lane / reach: silent-wrong · program.

    Rejected: (b) a P11g cell (order 17, lands alone with the R-DG snapshot churn; the consumption and the hang persist ~15 groups); (c) a new class issue (one arm; this tracker exists). (a) stacks under R-CP — the IRepr channel for user __repr__ lands on top of it; printed output changes once, from a value R-CF already rules wrong, so no new door.

  6. antonsynd commented on Oct 22, 2026

    @antonsynd
    OwnerAuthor

    Landed (chores band 2026-10-22) on dev @ 4d22212, PR #2346 to mainline. Whole-solution gate 81669/0/7 @ 4d22212 (measured), against 81093/0/7 @ be4664e.

    R-HO chore, commit 2f2c38f. Str and Repr run TryFormatSharpyInstance, keyed on PyFormat.IsSharpyStamped, before TryFormatPlainClrSequence. A Sharpy-compiled iterator is never formatted by enumerating it.

    Direction: print(c) on an __iter__/__next__ class was ['item', 'item'] with the later loop counting 0. It is now <__main__.Counter object> and the loop counts 2. The new fixture is builtins/iterable_instance_str_2057.

    Gate fallout: the new fixture shifted two sweep samples.

    Stays open as the user-__repr__ tracker under P11g (R-CP).

  7. added a commit that references this issue on Oct 22, 2026
  8. antonsynd commented on Oct 22, 2026

    @antonsynd
    OwnerAuthor

    Batching 2026-10-22 @ 4d22212 — charter .claude/plans/remediation-round-2026-09-20-batching.md §3.0, chores — call-28 band: generator-formatting cell (order 0, one commit, no plan; rides P21e's gate if no fixture .expected moves). Ruling R-IF (owner, 2026-10-22): D6 a — split: the formatting half is a cell here under R-HO; the multi-pass half is RECORDED.

    Cell (R-HO's contract: formatting a Sharpy-compiled object never iterates it): def gen() -> int: print("body ran"); yield 1; yield 2 then g = gen(); print(g) → observed body ran then [1, 2] @ be4664e (repr(g), the f-string hole and [g] likewise); expected <generator object gen at 0x…> with the body NOT run (python3 3.12). Lane silent-wrong, reach program. Found by the chores-band agent. Internals (read @ 4d22212): the R-HO arm is keyed on PyFormat.IsSharpyStamped and a compiler-synthesized iterator class carries no stamp.

    R-IF (a): a synthesized generator's str/repr is <generator object gen> (the P11g python-name channel owns the final spelling, R-CP/R-DG; the at 0x… address is deviation, R-CI) and formatting never enumerates it; Core.Tests theory site {str, repr, print, f-string hole, list element} × producer {generator function, generator method, synthesized generator expression} + one run fixture asserting the body did NOT run; mutation (unstamp → the generator row red); direction record body ran + [1, 2] → <generator object gen>; fixture .expected hit count FIRST. The multi-pass half — two loops over one g print body ran twice and count 4 where python counts 2, because the spec maps a generator to IEnumerable<T> (generators.md:61,72) — is RECORDED under R-CM: ledger row generator-multi-pass (audience python, severity info, executed example) + one generators.md sentence, in the same commit. Rejected: (b) promote multi-pass — a single-pass wrapper per call (changes the runtime behaviour of every re-iterating program, one-way; an allocation per call; the IEnumerable<T> return C# consumers see would have to change); (c) both halves as ledger rows, formatting kept (leaves a side effect, and a hang on an endless generator, that R-HO already ruled a bug). The ledger row is relaxable to (b) later; (a) stacks under (b). This issue stays P11g's user-__repr__ tracker (§3.17).

  9. antonsynd commented on Oct 22, 2026

    @antonsynd
    OwnerAuthor

    Landed: generator-formatting cell (R-IF under R-HO) @ 963b345; ledger row generator-multi-pass @ 97db52f (dev 0fb7b23, mainline b99221c). #2057 stays open as P11g's tracker.

    Cure:

    • Lowering records each generator function's python __qualname__ (IrCompilation.GeneratorQualifiedNames).
    • The emitter prints it as [global::Sharpy.SharpyGenerator("K.items")] on the iterator method.
    • Core reaches the stamp from the state machine through C#'s IteratorStateMachineAttribute.
    • TryFormatSharpyInstance, the one arm Str and Repr share, prints <generator object {qualname}> and never runs the body. The at 0x… address is omitted (R-CI).
    • An unstamped iterator (LINQ, any CLR iterator) keeps codegen: a CLR sequence in a BARE expression position prints a LINQ iterator's type name — the no-slot row of #1251 #1453's list rendering.
    • A generator expression already printed <iterator object> without enumerating; its spelling is P11g's.

    Direction record (run):

    Expression Before After
    print(g) body ran + [1, 2] <generator object gen>
    K().items() [3] <generator object K.items>
    K.Inner.walk (same shape) <generator object K.Inner.walk>
    local generator in main (same shape) <generator object main.<locals>.inner>

    Every line equals python3 3.12's minus the address.

    Guards:

    • Core.Tests: producer {module function, method, generic, local function} × site {str, repr, print, f-string hole, list element}.
    • Compiler.Tests GeneratorObjectFormattingTests, a programmatic run test, so no corpus pins move.

    Mutation records:

    • Emitter stamp removed: red, with the pre-fix output.
    • Core arm skipped: 14/46 red.

    Roster: the __qualname__ scope switch in LoweringPass.LowerStatement is a documented-by-design dispatch-site row @ 0fb7b23. The gate flagged it as unrostered. Mutation: the contract marker removed turns DesignCitations_ResolveToContractComments red.

    Fixture-hit count: 0 .expected files moved. Two C# snapshots gained the attribute line. Spy stdlib regenerated: Itertools.cs and Os.cs, attribute lines only.

    Ledger row generator-multi-pass (R-IF): two loops over one generator count 4 here and 2 in python. generators.md gains the sentence.

  10. antonsynd commented on Oct 22, 2026

    @antonsynd
    OwnerAuthor

    Cell (same channel, needs a ruling — D5, deviation by default): a user-declared union value has no str/repr today. Measured @ 0c90a73:

    union Shape:
        case Circle(r: int)
        case Empty
    
    def main() -> None:
        print(Shape.Circle(3))             # <__main__.Shape.Circle object>
        print(repr(Shape.Circle(3)))       # <__main__.Shape.Circle object>
        print([Shape.Circle(3), Shape.Empty()])   # [<__main__.Shape.Circle object>, <__main__.Shape.Empty object>]

    tagged_unions.md has no str/repr section and there is no Python oracle, so this is a ruling, not a bug. Proposed, extending Optional's constructor-spelling ruling (#2005): str and repr of a union value are both the constructor spelling with payload reprs — Shape.Circle(3), LoadState.Loaded('users.csv'), unit cases as Shape.Empty() (parenthesized, matching None()); Result's Ok(…)/Err(…) follow the same rule. Mojo 1.2 adopted the same default for its EnumLike types (Shape.circle(3) / repr → Shape.circle(Int(3)), unit cases without parentheses).

    Rung: the union lowering emits one class per case, so emit ToString() plus the IRepr channel on each; sequence after this issue's __str__/__repr__ split so a user-declared __str__ on a union keeps winning. Found reviewing the Mojo 1.2.0 release notes (docs/design/mojo-release-reviews/v1.2.0.md).


    Generated by Claude Code

  11. antonsynd commented on Oct 23, 2026

    @antonsynd
    OwnerAuthor

    Batching 2026-10-23 @ 6c95019 — charter .claude/plans/remediation-round-2026-09-20-batching.md §3.17, P11g — python-name/repr channel residue (order 17, this issue's channel). Ruling R-IM (owner, 2026-10-23): (a) PROMOTE — str and repr of a user-declared union value are both the constructor spelling with payload reprs — Shape.Circle(3), LoadState.Loaded('users.csv') — with unit cases PARENTHESIZED, Shape.Empty() (the spec's construction spelling, tagged_unions.md:107, and None()'s shape from #2005); Result follows the same rule (Ok(1) / Err('e'), with #2137); a user __str__/__repr__ on the union wins.

    Measured by the filer @ 0c90a73: print(Shape.Circle(3)), repr(...) and a list of union values all print <__main__.Shape.Circle object>. The surface was unlisted (R-CM, deviation until promoted); it is now a COMPARED row beside Optional/Template in python-fidelity-scope.md (R-IM). Rung: the union lowering emits one class per case, so ToString() plus the IRepr channel on each case class (P11g's mechanism, after this issue's __str__/__repr__ split so the user dunder keeps winning); a tagged_unions.md str/repr section with executed examples. Matrix: case shape {unit, one payload, several payloads, str payload, nested union payload} × site {str, repr, print, f-string hole, list element}; fixture-hit count first (no fixture asserts today's text — grep at the plan). Rejected: (b) RECORD the user-class default <module.Shape.Circle object> as a ledger row; (c) repr module-qualified (__main__.Shape.Circle(3)) — two spellings to pin for one value where Optional has one; the bare-unit variant (Shape.Empty, the pattern spelling) was offered and not taken. Printed output is one-way once a fixture pins it. Lane silent-wrong, reach program; the cell stays on this tracker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    class-trackerThe one issue per class: carries the contract and the cell tablelane:silent-wrongCompiles and runs with wrong output or semantics (rank first)reach:programAn ordinary program a Python programmer would write hits it

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions