Skip to content

semantic: a nested def that redefines a name in the same function scope is refused with a misleading SPY0220 at the call (was a CS0128 ICE) — rebind, overload or a clean refusal is undecided #2314

Closed

Description

@antonsynd

Class

Contract: a second def of an existing name in the same function scope has one decided meaning, and the compiler follows it on every route. The options are: rebind as Python does, overload by signature as a type-body def pair does (classes/overload_by_type), or refuse with a diagnostic that says "already defined". Today it has no decided meaning. The spec says nothing about it: no rebinding rule for def in docs/language_specification/ — grepped 2026-10-15.

Cells

axis values program observed expected lane reach found by
function scope × second def f with a different signature def main() -> None: / def f(x: int) -> int: return x + 1 / print(f(1)) / def f(x: str) -> str: return x + "!" / print(f("a")) @ 486c9a8: SPY0908 CS0128 (two C# local functions named F). @ cbabbf7: SPY0220 at f("a") — the second def now declares its own symbol (P54 #2250 Phase 2 task 3 stopped a nested def from writing the symbol it looked up), but the name still binds the first python3 3.12 prints 2 then a! (rebinding); or a clean "already defined" refusal; or an overload — needs a ruling false-refusal (was ice) program P54 Phase 2 task 3 direction probes (lane frozen)

Not measured: module-level and type-body twins with the SAME signature; a redefinition after a use inside a loop; the LSP route.

Acceptance

  • A ruling on rebind vs overload vs refusal, recorded on this issue.
  • Every cell in the table follows the ruling: no ICE, and no type error at a call site that only exists because the second def was ignored.

Activity

  1. added
    lane:false-refusalA valid program (spec or python) is refused
    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 Oct 15, 2026
  2. antonsynd commented on Oct 15, 2026

    @antonsynd
    OwnerAuthor

    Cell: the CS0128 internal error is still reachable at HEAD when every call fits the FIRST def. Found by the spec-block gate on the P54 push (#2250, PR #2318).

    The spec-block gate wraps a module-level overload example in main(), which turns it into this class's shape. Direction under sharpyc run:

    Wrapped block 486c9a8 7ae6ed7
    function_parameters.md:142 — def f(x: int) / def f(x: float), then f(42), f(3.14) SPY0908 CS0128 SPY0220 at f(3.14)
    overload_resolution.md:261 — same shape, returns str SPY0908 CS0128 SPY0220 at f(3.14)
    overload_resolution.md:320 — six pairs (f(b: uint8)/f(s: int16), q, g, r, h, p); every call type-checks against the first def SPY0220 at p(LIMIT) (the second def p(s: str) had overwritten the first) SPY0908 CS0128 ("a local function named 'F' is already defined")

    So the class has two faces at HEAD:

    • a misleading SPY0220 when a call fits only a later def;
    • the CS0128 internal error when every call fits the first def.

    The first two blocks are allowlisted against this issue (59ed0a8), and :320's old #1952 entry was deleted as stale. A ruling here (rebind, overload, or a clean refusal; C# refuses with CS0128) drains them. The spec examples are module-level overload sets, so they are correct as written: the cure is in the checker, or in the gate's wrapping. The spec text does not change.

  3. antonsynd commented on Oct 15, 2026

    @antonsynd
    OwnerAuthor

    Two more cells @ 045ce6b (P54 /verify-implementation sibling-cell prober, axis "binding kind of a builtin/function name"):

    # program @ 486c9a8 @ 045ce6b note
    1 def main() -> None: / def f(x: int) -> int: / return x + 1 / def f(x: str) -> str: / return x + "!" / print(f(1)) — a call that fits the FIRST def only SPY0220 (exit 1) SPY0908 CS0128 (exit 2) the inverse of the flip the table above records (call-the-second went CS0128 → SPY0220 at cbabbf7); a refusal became an ICE — a regression in KIND inside this class, caused by the own-symbol rule (every nested def now declares its own symbol, so both F locals are emitted). python3 3.12 prints TypeError at the call (rebinding)
    2 an overloaded def len pair inside an if in a def, with no call SPY0908 CS0128 SPY0908 CS0128 pre-existing

    Both cells belong to this issue's ruling (rebind / overload / refuse); a fix lane for #2250 did not patch them, per the verify round's no-widening rule.

  4. antonsynd commented on Oct 15, 2026

    @antonsynd
    OwnerAuthor

    Batching 2026-10-15 @ f8c37f9 — charter .claude/plans/remediation-round-2026-09-20-batching.md §3.20, P21e — declaration-position totality (order 6c′, directly after the chores band, before P69). Ruling R-HJ (owner, 2026-10-15): (a) refuse — a second def f in the SAME block is SPY0204 "function 'f' is already defined in this scope".

    Internals (grep-only @ f8c37f9): C# local functions cannot be overloaded (CS0128); the spec has no rebinding rule for def and its only overload rule is for module and type bodies; SPY0204 DuplicateDefinition is the existing "already defined" family (R-CY reuses it for import m beside def m). python3 3.12 prints 2 then a! (rebinding). Mechanism: one validator arm at the declaration-position seam P21e builds; a def in a SIBLING block is a fresh binding (Python and C# agree); a def in a NESTED block shadowing an enclosing one follows B2 — the plan measures whether the emitter's name versioning covers local functions or Roslyn raises CS0136, in which case a Sharpy refusal pre-empts it. Guard: TypeDefinitionOwnSymbolMatrixTests (P54) gains the kind "redefined def" × block relation {same, sibling, nested} × signature {same, different} × call {fits first, fits second, none}; mutation (drop the arm → the same-block cells red: CS0128 or SPY0220). Direction record: CS0128 / SPY0220 → SPY0204; no running program is refused (every same-block pair is CS0128 at HEAD). The two spec_blocks_allowlist.txt rows citing this issue are correct module-level overload examples the gate wraps in main(): under this ruling they become a P20 gate-wrapping question (re-cite #1952), not a cell here. Rejected: (b) overload by signature as at module/type level (a materialized C# spelling for local functions; g = f ambiguous — one-way once relied on); (c) rebind as Python does (the first def dead code; the one mutable construct-owned binding, against B3/B4 — one-way). (a) is relaxable to (b) later. Lane / reach: false-refusal + ice · program. Class tracker: #2063 (this issue carries the ruling and its own cells). Pulled forward with the group by R-HK (owner: D2 a).

  5. antonsynd commented on Oct 23, 2026

    @antonsynd
    OwnerAuthor

    Closed by P21e (plan-f8f73c). Implementation 03eee7528..e4ed43321; verify round ..6c95019b8.

    Verdict (R-HJ, measured branch): a nested def that re-declares a name already declared by a def of the same function in the same block, an enclosing block or an enclosed block is SPY0204 at the later def's name. Sibling blocks are fresh bindings: if/elif/else, try/except/else/finally, a loop body and its else, and, by the owner's ruling at the verify round (2026-10-23), the arms of a match. That ruling reverts the lead's one-scope ruling D5b in 8fc618b. D5b refused an is-chain-lowered match that ran at base, and Rust and Swift both scope match arms. When the match lowers to one C# switch, a def per arm is still SPY0908 CS0128, because local functions are not versioned. That is filed by class as #2361 and allowlisted there with the fingerprint required.

    D6, recorded as the decided verdict: a call that fits only the refused second def keeps its SPY0220 cascade, just as a module-level duplicate does. Under refusal the second def is ignored by ruling, which supersedes this issue's pre-ruling acceptance bullet.

    Probes under sharpyc run @ e4ed433 (base 03eee75 in parentheses):

    • i, def f(int) then def f(str) in one block: SPY0204 @4:9 (SPY0908 CS0128)
    • i2, the same signature twice: SPY0204 @4:9 (CS0128)
    • o, the call fits the second def: SPY0204 @4:9 + SPY0220 @6:13 (SPY0220 only)
    • k, nested outer-first: SPY0204 @5:13 (CS0136)
    • y, nested inner-first: SPY0204 @6:9 (CS0136)
    • j, sibling if/else blocks: runs, prints 2 / a! (unchanged)

    Guard: the nested-def sub-matrix of TypeDefinitionOwnSymbolMatrixTests, 8 relations × 2 signatures × 3 calls = 48 cells. It has zero #2314 rows; its 6 #2361 rows require the CS0128 fingerprint. Mutations: def arm off → 24 cells red, and the nested_def_redefined_* fixtures fail with SPY0908; same-block-only → 12 red; siblings as one scope → 18 red (direction control); D5b restored → 12 red; restored → green.

    Spec: function_definition.md § Nested functions (an SPY0204 error block and an executed sibling block). The two spec-block rows were re-cited to #1952 as gate-wrapping, with SPY0204 as their first error.

    FINAL gate: whole solution @ 8fc618b: Core 4035/0/0, Stdlib 4294/0/0, Cli 1126/0/1, Compiler 50483/0/7, all measured. Lsp.Tests was red only in the 6 corpus-census pins (the removed D5b fixture stem) and is 22039/0/0 @ 6c95019 after the measured repin, with SHARPY_ROUTE_SWEEP_PINS_REQUIRED=1. Base gate 81988/0/8 @ e4ed433 (measured). Spec-block gate 1216/0 failing, allowlist issue-state, staleness and format checks are all clean. The commits are on dev (not yet pushed).

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:false-refusalA valid program (spec or python) is refusedreach: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