# Sharpy Deviation Catalog
#
# This file documents known behavioral deviations between Sharpy and the
# languages it draws from (Python syntactically, C# semantically). It is a
# documentation/planning artifact for the diagnostic transition system —
# it is NOT compiled into the compiler.
#
# Schema (per entry):
#   id                  — unique kebab-case identifier
#   code                — diagnostic code if applicable, null if no diagnostic
#   category            — scoping | types | operators | syntax | semantics | stdlib
#   audience            — python | csharp | both
#   severity            — error | warning | hint | info | none
#   python_behavior     — what Python does
#   sharpy_behavior     — what Sharpy does
#   spec_ref            — relative path to spec section (or null)
#   existing_diagnostic — diagnostic code that already covers this (or null)
#   planned_diagnostic  — planned new diagnostic from transition system (or null)
#   example.python      — short Python snippet showing the deviation
#   example.sharpy      — short Sharpy snippet (or comment) showing Sharpy behavior
#
# Standard: every example line is EXECUTED against HEAD (python3 for the Python side, the
# compiler for the Sharpy side) before its entry is committed — an entry whose examples were
# only reasoned about documents a deviation that may not exist.
#
# Categories:
#   scoping    — variable visibility / lifetime
#   types      — type system, type rules, generics, protocols
#   operators  — operator behavior / overloading / precedence
#   syntax     — surface syntax accepted/rejected
#   semantics  — runtime semantics that differ silently or with diagnostics
#   stdlib     — standard library / builtin behavior
#
# Severity reflects the *current* compiler behavior. "none" = silent deviation
# (no diagnostic today); "hint" = planned hint-level transition diagnostic.

deviations:

  # ============================================================
  # Hard-rejected Python syntax (already produces errors)
  # ============================================================

  - id: global-nonlocal-keywords
    code: SPY0134
    category: syntax
    audience: python
    severity: error
    python_behavior: "`global` and `nonlocal` declarations rebind names in outer scopes."
    sharpy_behavior: "`global` and `nonlocal` are rejected. C# scoping rules apply; use mutable containers or return values."
    spec_ref: docs/language_specification/variable_scoping.md
    existing_diagnostic: SPY0134
    planned_diagnostic: null
    example:
      python: |
        x = 1
        def f():
            global x
            x = 2
      sharpy: |
        x = 1
        def f():
            global x  # ERROR SPY0134: 'global' keyword is not supported in Sharpy
            x = 2

  - id: raise-from
    code: SPY0122
    category: syntax
    audience: python
    severity: error
    python_behavior: "`raise NewError() from cause` chains exceptions via __cause__."
    sharpy_behavior: "`raise ... from ...` is not supported. Use the inner-exception constructor argument or `try/except` to wrap exceptions."
    spec_ref: docs/language_specification/exception_handling.md
    existing_diagnostic: SPY0122
    planned_diagnostic: null
    example:
      python: |
        try:
            do_thing()
        except IOError as e:
            raise RuntimeError("wrapped") from e
      sharpy: |
        try:
            do_thing()
        except IOError as e:
            raise RuntimeError("wrapped", e)  # pass cause as arg instead

  - id: free-union-syntax
    code: SPY0113
    category: types
    audience: python
    severity: error
    python_behavior: "`int | str` and `Union[int, str]` are first-class union types in Python typing."
    sharpy_behavior: "Free unions are rejected, deliberately and permanently (#992, closed as not planned). A free union is an untagged set of possibilities the compiler must narrow at every use; Sharpy's three sanctioned shapes each make the discrimination explicit — `T?` when the second case is absence, a `union` declaration when the cases are named alternatives, and `Result[T, E]` when one alternative is a failure. Axiom precedence: type safety (3) > Python (2)."
    spec_ref: docs/language_specification/tagged_unions.md
    existing_diagnostic: SPY0113
    planned_diagnostic: null
    example:
      python: |
        def parse(x: int | str) -> int: ...
      sharpy: |
        # ERROR SPY0113: free union types are not supported.
        # The three sanctioned shapes, by what the second case MEANS:
        union IntOrStr:              # named alternatives
            case AsInt(value: int)
            case AsStr(value: str)

        def find(k: str) -> int?:    # absence
            return None

        def parse(s: str) -> int !ValueError:   # failure
            return Ok(0)

  - id: dict-spread-in-call
    code: SPY0123
    category: syntax
    audience: python
    severity: error
    python_behavior: "`f(**kwargs)` spreads a dict into keyword arguments."
    sharpy_behavior: "Dict spread (`**`) in calls is rejected. Pass explicit keyword arguments or an options struct."
    spec_ref: docs/language_specification/flexible_arguments.md
    existing_diagnostic: SPY0123
    planned_diagnostic: null
    example:
      python: |
        opts = {"host": "x", "port": 80}
        connect(**opts)
      sharpy: |
        # ERROR SPY0123: dict spread in calls is not supported
        connect(host="x", port=80)

  - id: empty-list-shorthand
    code: SPY0114
    category: syntax
    audience: python
    severity: error
    python_behavior: "`x = []` produces an empty list with inferred element type."
    sharpy_behavior: "Bare `[]` is rejected; an explicit element type is required (e.g., `list[int]()` or `[]: list[int]`)."
    spec_ref: docs/language_specification/collection_types.md
    existing_diagnostic: SPY0114
    planned_diagnostic: null
    example:
      python: |
        xs = []
      sharpy: |
        xs: list[int] = []     # OK — annotation provides element type
        ys = list[int]()       # OK — constructor form

  - id: empty-set-dict-shorthand
    code: SPY0115
    category: syntax
    audience: python
    severity: error
    python_behavior: "`{}` is an empty dict; `set()` is required for an empty set."
    sharpy_behavior: "Bare `{}` for an empty dict/set is rejected; explicit constructor with type arguments is required."
    spec_ref: docs/language_specification/collection_types.md
    existing_diagnostic: SPY0115
    planned_diagnostic: null
    example:
      python: |
        d = {}
      sharpy: |
        d = dict[str, int]()  # explicit constructor required

  - id: call-syntax-only-forms-as-values
    code: SPY0337
    category: types
    audience: python
    severity: error
    python_behavior: "`isinstance` is an ordinary builtin function, so `g = isinstance; g(x, T)` works. A class referenced by name is a first-class callable object."
    sharpy_behavior: "`isinstance` is a compile-time narrowing construct, and a union variant constructor (`Shape.Circle`) names a case rather than a callable. Both are call syntax only and are rejected in value position; wrap in a lambda to pass one around. Parenthesized calls of both forms work exactly like the direct forms, narrowing included."
    spec_ref: docs/language_specification/type_narrowing.md
    existing_diagnostic: SPY0337
    planned_diagnostic: null
    example:
      python: |
        g = isinstance
        print(g(shape, Circle))
      sharpy: |
        g = isinstance          # ERROR SPY0337: must be called as a function
        mk = Shape.Circle       # ERROR SPY0337: union variant constructor
        g = lambda v: isinstance(v, Circle)   # OK — lambda pins the call
        print(isinstance(shape, Circle))      # OK — direct call narrows
        print((isinstance)(shape, Circle))    # OK — parentheses change nothing

  # ============================================================
  # Block / comprehension scoping (enhanced SPY0200)
  # ============================================================

  - id: block-scoping-if
    code: SPY0200
    category: scoping
    audience: python
    severity: error
    python_behavior: "Variables declared inside `if`/`elif`/`else` bodies leak into the surrounding scope."
    sharpy_behavior: "Compound-statement bodies introduce a new scope; variables declared inside are not visible outside the block."
    spec_ref: docs/language_specification/variable_scoping.md
    existing_diagnostic: SPY0200
    planned_diagnostic: null
    example:
      python: |
        if cond:
            x = 1
        print(x)  # works in Python
      sharpy: |
        if cond:
            x = 1
        print(x)  # ERROR SPY0200: undefined identifier 'x' (block-scoped)

  - id: block-scoping-for-loop-variable
    code: SPY0200
    category: scoping
    audience: python
    severity: error
    python_behavior: "The for-loop iteration variable persists after the loop ends."
    sharpy_behavior: "The for-loop iteration variable is scoped to the loop body only."
    spec_ref: docs/language_specification/variable_scoping.md
    existing_diagnostic: SPY0200
    planned_diagnostic: null
    example:
      python: |
        for i in range(5):
            pass
        print(i)  # 4 in Python
      sharpy: |
        for i in range(5):
            pass
        print(i)  # ERROR SPY0200: 'i' is block-scoped to the for loop

  - id: block-scoping-while
    code: SPY0200
    category: scoping
    audience: python
    severity: error
    python_behavior: "Variables declared inside a `while` body remain visible after the loop."
    sharpy_behavior: "Variables declared inside a `while` body are not visible after the loop."
    spec_ref: docs/language_specification/variable_scoping.md
    existing_diagnostic: SPY0200
    planned_diagnostic: null
    example:
      python: |
        while cond():
            result = compute()
        print(result)
      sharpy: |
        result: int = 0
        while cond():
            result = compute()
        print(result)  # declare 'result' outside the loop

  - id: block-scoping-try-except
    code: SPY0200
    category: scoping
    audience: python
    severity: error
    python_behavior: "Variables declared in a `try` body are visible in `except`/`else`/`finally` and after the statement."
    sharpy_behavior: "Each clause (`try`, `except`, `else`, `finally`) is its own scope. Cross-clause variables must be declared before the `try` statement."
    spec_ref: docs/language_specification/variable_scoping.md
    existing_diagnostic: SPY0200
    planned_diagnostic: null
    example:
      python: |
        try:
            result = risky()
        except ValueError:
            result = -1
        print(result)
      sharpy: |
        result: int = 0
        try:
            result = risky()
        except ValueError:
            result = -1
        print(result)

  - id: block-scoping-with
    code: SPY0200
    category: scoping
    audience: python
    severity: error
    python_behavior: "Variables declared inside `with` (including the `as` binding) remain accessible after the block."
    sharpy_behavior: "`with` body and `as` binding are block-scoped."
    spec_ref: docs/language_specification/variable_scoping.md
    existing_diagnostic: SPY0200
    planned_diagnostic: null
    example:
      python: |
        with open("f.txt") as f:
            data = f.read()
        print(data)
      sharpy: |
        with open("f.txt") as f:
            data = f.read()
            print(data)  # use 'data' inside the block

  - id: comprehension-loop-variable-leak
    code: SPY0200
    category: scoping
    audience: python
    severity: error
    python_behavior: "In Python 3, comprehension loop variables do not leak (matches Sharpy). In Python 2 and generator expressions in some contexts they could."
    sharpy_behavior: "Comprehension loop variables are local to the comprehension (matches Python 3)."
    spec_ref: docs/language_specification/comprehensions.md
    existing_diagnostic: SPY0200
    planned_diagnostic: null
    example:
      python: |
        xs = [x * 2 for x in range(5)]
        # 'x' is not visible here in Python 3 either
      sharpy: |
        xs = [x * 2 for x in range(5)]
        # 'x' is not visible here

  - id: walrus-in-comprehension-scope
    code: SPY0200
    category: scoping
    audience: python
    severity: error
    python_behavior: "Python 3.8+: walrus (`:=`) inside a comprehension binds in the *enclosing* scope and leaks out."
    sharpy_behavior: "Walrus inside a comprehension is local to the comprehension. The syntactic boundary equals the semantic boundary."
    spec_ref: docs/language_specification/walrus_operator.md
    existing_diagnostic: SPY0200
    planned_diagnostic: null
    example:
      python: |
        results = [y for x in data if (y := transform(x)) > 0]
        print(y)  # leaks in Python 3.8+
      sharpy: |
        results = [y for x in data if (y := transform(x)) > 0]
        print(y)  # ERROR SPY0200: 'y' is comprehension-local

  - id: builtin-type-name-shadowing-refused
    code: SPY0212
    category: scoping
    audience: python
    severity: error
    python_behavior: "A class statement may take any builtin name: `class double:` rebinds `double` in the module namespace, and annotations — which CPython does not resolve statically — are unaffected."
    sharpy_behavior: "A TYPE declaration (class/struct/interface/enum/union/delegate) may not take the bare spelling of a builtin TYPE name. A type declaration enters the namespace annotations resolve through, so `x: double` would become ambiguous, and Sharpy resolves annotations statically. Backtick-escape the name to declare a user type with that spelling. Bindings in VALUE position are NOT refused — see `builtin-name-shadowing-in-value-position`."
    spec_ref: docs/language_specification/name_mangling.md
    existing_diagnostic: SPY0212
    planned_diagnostic: null
    example:
      python: |
        class double:
            def __init__(self, v):
                self.v = v
        print(double(3).v)  # 3
      sharpy: |
        class double:          # ERROR SPY0212: 'double' is a builtin type name
            v: int

        class `double`:        # OK — the escaped spelling is a user symbol
            v: int

            def __init__(self, v: int):
                self.v = v

        def main():
            print(`double`(3).v)  # 3

  - id: builtin-name-shadowing-in-value-position
    code: SPY0483
    category: scoping
    audience: python
    severity: warning
    python_behavior: "Rebinding a builtin name is silent and late-bound: `len = 5` at module level changes what `len` means for every later use in that module, including uses in functions defined earlier."
    sharpy_behavior: "Allowed and honored, but warned. A variable, constant, parameter, for-target (including a comprehension's), walrus (`:=`) target, inline `out` declaration or function declaration may spell a builtin name; the binding shadows the builtin lexically, as any inner binding shadows an outer one — statically, not late-bound. The builtin is then unreachable by its bare spelling in that scope. Backtick-escape the declaration to keep both spellings usable — in every one of those forms. Class MEMBERS are not warned: a field or method is reached through `self.`, so it never competes for a bare spelling."
    spec_ref: docs/language_specification/name_mangling.md
    existing_diagnostic: SPY0483
    planned_diagnostic: null
    example:
      python: |
        def double(x):
            return x * 2
        print(double(21))  # 42 — silent, no diagnostic
      sharpy: |
        def double(x: int) -> int:  # warning SPY0483: 'double' is a builtin name
            return x * 2

        def main():
            print(double(21))  # 42 — the user function, as written

  # ============================================================
  # Planned transition hints (SPY0470 - SPY0476)
  # ============================================================

  - id: utf16-string-length
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "`len(s)` returns the number of Unicode code points; indexing yields a single-character string per code point."
    sharpy_behavior: "`len(s)` returns the number of UTF-16 code units (matching .NET `string.Length`); indexing yields a single UTF-16 code unit. Surrogate pairs count as 2. Axiom precedence: .NET (1) > Python (2)."
    spec_ref: docs/language_specification/string_type.md
    existing_diagnostic: null
    planned_diagnostic: SPY0470
    example:
      python: |
        len("😀")        # 1
        "😀"[0]          # '😀'
      sharpy: |
        len("😀")        # 2  (UTF-16 code units)
        "😀"[0]          # high-surrogate code unit '\uD83D'

  - id: struct-value-semantics
    code: null
    category: semantics
    audience: python
    severity: none
    python_behavior: "All user-defined classes have reference semantics; assignment shares the same object."
    sharpy_behavior: "`struct` declarations have value semantics (C# `struct`). Assignment / argument passing copies the value."
    spec_ref: docs/language_specification/structs.md
    existing_diagnostic: null
    planned_diagnostic: SPY0471
    example:
      python: |
        class Point:
            x: int
            y: int
        a = Point(1, 2)
        b = a          # 'b' aliases 'a'
        b.x = 99
        # a.x == 99
      sharpy: |
        struct Point:
            x: int
            y: int
        a = Point(1, 2)
        b = a          # 'b' is a *copy* of 'a'
        b.x = 99
        # a.x == 1     (unchanged)

  - id: homogeneous-variadic-args
    code: SPY0220
    category: types
    audience: python
    severity: error
    python_behavior: "`*args` accepts arbitrary heterogeneous values; the function sees a tuple of `object`-typed elements."
    sharpy_behavior: "`*args: T` is homogeneously typed — every passed argument must be assignable to `T`."
    spec_ref: docs/language_specification/function_variadic_arguments.md
    existing_diagnostic: SPY0220
    planned_diagnostic: SPY0472
    example:
      python: |
        def log(*args):
            for a in args: print(a)
        log(1, "two", 3.0)        # OK in Python
      sharpy: |
        def log(*args: int) -> None:
            for a in args:
                print(a)
        log(1, "two", 3.0)
        # ERROR SPY0220: argument 'two' (str) not assignable to int

  - id: variadic-args-is-array-not-tuple
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "Inside the function, `*args` is a **tuple**: `args` prints as `(1, 2, 3)`, slicing it yields a tuple, and an empty call gives `()`."
    sharpy_behavior: "`*args: T` binds as an `array[T]` — the CLR `params T[]` backing shown through. `len()`, indexing and iteration behave the same; slicing yields a `list[T]` (per array slicing), and an empty call gives an empty array, not `()`. This is the CONTAINER half of the variadic difference; the element half is `homogeneous-variadic-args`, which is what refuses a heterogeneous call."
    spec_ref: docs/language_specification/function_variadic_arguments.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        def f(*args):
            print(len(args))
            print(args[0])
            print(args[1:])       # a TUPLE: (2, 3)
        f(1, 2, 3)
      sharpy: |
        def f(*args: int) -> None:
            print(len(args))
            print(args[0])
            print(args[1:])       # a list[int]: [2, 3]
        f(1, 2, 3)
        # Same three values; the container differs. `args` is array[int], not a tuple.

  - id: no-classmethod
    code: null
    category: syntax
    audience: python
    severity: none
    python_behavior: "`@classmethod` defines a method whose first argument is the class object."
    sharpy_behavior: "Only instance methods, static methods, and dunder methods exist. There is no `@classmethod`. Use a static method (no `self`) and reference the type directly when needed."
    spec_ref: docs/language_specification/class_methods.md
    existing_diagnostic: null
    planned_diagnostic: SPY0473
    example:
      python: |
        class Foo:
            @classmethod
            def make(cls) -> "Foo":
                return cls()
      sharpy: |
        class Foo:
            # static method — no 'self'
            def make() -> Foo:
                return Foo()

  - id: no-async-generator-expressions
    code: null
    category: syntax
    audience: python
    severity: none
    python_behavior: "`(x async for x in src)` is an async generator expression producing a lazy async iterator."
    sharpy_behavior: "Sharpy has no generator-expression construct (synchronous genexprs are likewise unavailable), so async generator expressions are unsupported. Async list/set/dict comprehensions ARE supported inside `async def` — use one of those, or an explicit `async for` loop and `append`."
    spec_ref: docs/language_specification/async_programming.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        agen = (x async for x in source())
      sharpy: |
        # use an async list comprehension instead
        results: list[int] = [x async for x in source()]

  - id: isinstance-tuple-is-a-tuple-type
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "`isinstance(x, (A, B))` checks whether `x` is an instance of `A` or `B` (any-of). `isinstance(x, (int))` collapses to `isinstance(x, int)`. `isinstance(x, int or str)` is legal but evaluates `int or str` to `int` (truthy short-circuit). `isinstance(x, tuple[int, str])` raises `TypeError: isinstance() argument 2 cannot be a parameterized generic`."
    sharpy_behavior: "The second argument to `isinstance` is a type position. `(A, B)` denotes `tuple[A, B]` — a structural `ValueTuple` runtime test that narrows to `tuple[A, B]` on success. `(T)` denotes `tuple[T]`. `isinstance(x, tuple[int, str])` is the canonical spelling and works. Non-type expressions (including `int or str`) are refused with SPY0344. This is a same-spelling-different-meaning deviation: the tuple form has identical syntax but tests a structural tuple type rather than checking any-of."
    spec_ref: docs/language_specification/builtin_functions.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        isinstance(1, (int, str))        # True (any-of check)
        isinstance(1, (int))             # True (parens collapse to int)
        isinstance('a', int or str)      # False (int or str -> int)
      sharpy: |
        isinstance(x, (int, str))        # tests tuple[int, str]
        isinstance(x, (int))            # tests tuple[int]
        isinstance(x, int or str)        # SPY0344: not a type expression

  - id: isinstance-non-type-expression
    code: SPY0344
    category: types
    audience: python
    severity: error
    python_behavior: "`isinstance(x, int or str)` is syntactically legal (evaluates `int or str` to `int`)."
    sharpy_behavior: "The second argument must be a type expression. Non-type expressions — including `int or str` — are refused with SPY0344. For an any-of check, write `isinstance(x, int) or isinstance(x, str)`; note that `(A, B)` denotes the tuple type `tuple[A, B]`."
    spec_ref: docs/language_specification/builtin_functions.md
    existing_diagnostic: SPY0344
    planned_diagnostic: null
    example:
      python: |
        if isinstance(x, int or str):
            ...
      sharpy: |
        if isinstance(x, int) or isinstance(x, str):
            ...
        # Note: 'or' does not narrow — see type_narrowing.md

  - id: isinstance-open-generic
    code: SPY0345
    category: types
    audience: python
    severity: error
    python_behavior: "`isinstance(b, Box)` accepts a bare generic class (generics are erased at runtime), and `isinstance(b, Box[int])` raises TypeError. The same erasure makes `except MyError:` and `case Box():` accept bare generic names too."
    sharpy_behavior: "Reversed, because .NET reifies generics. A bare generic name is accepted only when the SUBJECT's own static type determines the type arguments (a `Box[int]` tested against `Box` tests `Box[int]`); otherwise it is rejected, because there is no single runtime `Box` and a successful test could not narrow to a spellable type. The closed spelling `isinstance(x, Box[int])` is the supported form and does narrow. The rule covers every position that names a type to test against — `isinstance(x, T)`, `x as? T` / `x as! T`, a match class pattern `case T():`, and `except T:` — with the subject differing per site: the tested value for isinstance/as, the scrutinee for a match pattern, and nothing at all for an except clause, so a bare generic exception name is always rejected. (`x is T` is not a type-test position: `is` compares references, and that spelling draws SPY0349.) A match pattern is the one site where the closed spelling is unavailable (the parser rejects type arguments in patterns, SPY0125), so its remedy is to bind the value to a typed local first or to match a non-generic base."
    spec_ref: docs/language_specification/builtin_functions.md
    existing_diagnostic: SPY0345
    planned_diagnostic: null
    example:
      python: |
        if isinstance(x, Box):        # any parameterization
            ...
      sharpy: |
        if isinstance(x, Box[int]):   # names the runtime type; narrows to Box[int]
            ...

  - id: no-negative-tuple-indexing
    code: SPY0259
    category: types
    audience: python
    severity: error
    python_behavior: "`t[-1]` returns the last element of a tuple."
    sharpy_behavior: "Tuples are statically typed; negative indices on a tuple are rejected. Use the literal positive index."
    spec_ref: docs/language_specification/type_hierarchy.md
    existing_diagnostic: SPY0259
    planned_diagnostic: SPY0476
    example:
      python: |
        t = (1, 2, 3)
        last = t[-1]      # 3
      sharpy: |
        t: tuple[int, int, int] = (1, 2, 3)
        last = t[-1]      # ERROR SPY0259: negative tuple index not allowed
        last = t[2]       # OK

  # ============================================================
  # Other deviations (no current or planned diagnostic, or
  # covered by an existing diagnostic)
  # ============================================================

  - id: failable-cast-operators
    code: null
    category: operators
    audience: both
    severity: none
    python_behavior: "No cast operator; casting is via constructor calls (`int(x)`) or `typing.cast` (a no-op hint)."
    sharpy_behavior: "The `as?`/`as!` failable-cast operators put the failure mode on the operator: `x as! T` throws, `x as? T` yields `T?`. They are unconditional language surface — graduated from the experimental `failable_cast` flag in #1096, and the flag name was deleted from the registry in #1128. The legacy `to`/`to?` spelling was retired in #1127 and is now a parse error, so the SPY0479 migration hint that once steered `to` toward `as` no longer exists (the code is reserved, never reused). A nullable target on `as?`/`as!` (`x as? T?`) is a hard error, SPY0334."
    spec_ref: docs/language_specification/type_casting.md
    existing_diagnostic: SPY0334
    planned_diagnostic: null
    example:
      python: |
        value = int(x)          # narrowing via constructor
      sharpy: |
        value = x as! int       # throws InvalidCastException on failure
        maybe_value = x as? int # int? — None on failure
        legacy = x to int       # ERROR: `to` was retired (#1127); use `as!`/`as?`

  - id: string-indexing-utf16
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "`s[i]` indexes by Unicode code point."
    sharpy_behavior: "`s[i]` indexes by UTF-16 code unit; multi-code-unit characters require surrogate-pair handling. Axiom precedence: .NET (1) > Python (2)."
    spec_ref: docs/language_specification/string_type.md
    existing_diagnostic: null
    planned_diagnostic: SPY0470
    example:
      python: |
        "café"[3]    # 'é'
      sharpy: |
        "café"[3]    # 'é' (BMP — single code unit)
        "𝄞ab"[0]     # high surrogate, not the G clef glyph

  - id: duck-typing-rejected
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "Methods are resolved structurally at runtime — any object with the right method works."
    sharpy_behavior: "Polymorphism requires explicit interfaces or base classes. Duck typing is rejected in favor of nominal typing (Axioms 1+3)."
    spec_ref: docs/language_specification/interfaces.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        def quack(d):
            d.quack()       # works for any object with .quack()
      sharpy: |
        class IQuacker:
            def quack(self) -> None: ...
        def quack(d: IQuacker) -> None:
            d.quack()

  - id: no-multiple-class-inheritance
    code: SPY0281
    category: types
    audience: python
    severity: error
    python_behavior: "Classes may inherit from multiple base classes (with MRO/C3 linearization)."
    sharpy_behavior: "Single class inheritance only. A class may implement multiple interfaces, but extend at most one base class."
    spec_ref: docs/language_specification/inheritance.md
    existing_diagnostic: SPY0281
    planned_diagnostic: null
    example:
      python: |
        class C(A, B):  # multiple inheritance
            pass
      sharpy: |
        class C(A, IB, IC):  # one base class + interfaces only
            pass

  - id: no-metaclasses
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "Classes may declare a `metaclass=` to customize class creation."
    sharpy_behavior: "Metaclasses are not supported. Use generators / source generators or hand-written code."
    spec_ref: docs/language_specification/classes.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        class Meta(type): ...
        class Foo(metaclass=Meta): ...
      sharpy: |
        # No equivalent. Use code generation or a base class.
        class Foo: ...

  - id: no-descriptors
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "Objects implementing `__get__`/`__set__`/`__delete__` participate in attribute lookup as descriptors."
    sharpy_behavior: "The descriptor protocol is not supported. Use properties (function-style or auto) for controlled attribute access."
    spec_ref: docs/language_specification/properties.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        class Lazy:
            def __get__(self, obj, owner): ...
      sharpy: |
        class Foo:
            @property
            def value(self) -> int: ...

  - id: explicit-type-annotations-required
    code: SPY0226
    category: types
    audience: python
    severity: error
    python_behavior: "Type annotations are optional; untyped code is fully dynamic."
    sharpy_behavior: "Type annotations are required for parameters, return types, and any variable that cannot be inferred from the initializer."
    spec_ref: docs/language_specification/type_annotations.md
    existing_diagnostic: SPY0226
    planned_diagnostic: null
    example:
      python: |
        def add(a, b):
            return a + b
      sharpy: |
        def add(a: int, b: int) -> int:
            return a + b

  - id: no-double-star-kwargs
    code: null
    category: syntax
    audience: python
    severity: none
    python_behavior: "`**kwargs` accepts arbitrary keyword arguments as a `dict[str, Any]`."
    sharpy_behavior: "`**kwargs` is not supported. Use named parameters with defaults or an explicit options struct."
    spec_ref: docs/language_specification/flexible_arguments.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        def configure(**kwargs):
            host = kwargs.get("host", "localhost")
      sharpy: |
        def configure(*, host: str = "localhost", port: int = 80) -> None:
            ...

  - id: tuple-immutability-typed
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "Tuples are runtime-immutable but indexed dynamically (any element type)."
    sharpy_behavior: "Tuples have positionally-typed elements (`tuple[int, str]`); each index has a known static type. The tuple itself is immutable."
    spec_ref: docs/language_specification/type_hierarchy.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        t = (1, "two")
        x = t[0] + 1     # int + int OK at runtime
      sharpy: |
        t: tuple[int, str] = (1, "two")
        x = t[0] + 1     # statically typed int

  - id: optional-chaining-no-flatten
    code: null
    category: operators
    audience: both
    severity: none
    python_behavior: "Python has no `?.` operator; chained nullable navigation is manual."
    sharpy_behavior: "`a?.b?.c` short-circuits to `None` on any null link. The result type is `T?` (the underlying member type lifted, not flattened — chained `??` already produces `T?`)."
    spec_ref: docs/language_specification/null_conditional_access.md
    existing_diagnostic: SPY0236
    planned_diagnostic: null
    example:
      python: |
        v = a.b.c if a and a.b else None
      sharpy: |
        v = a?.b?.c           # type is T?

  - id: no-stopiteration
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "Iterators raise `StopIteration` to signal exhaustion. `next()` propagates this exception."
    sharpy_behavior: "Iteration uses the .NET `IEnumerator<T>` pattern (`MoveNext()` returns `bool`). `StopIteration` is not raised; `next()` returns `Optional[T]` or accepts a default."
    spec_ref: docs/language_specification/builtin_functions.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        it = iter([1, 2])
        next(it)         # 1
        next(it)         # 2
        next(it)         # raises StopIteration
      sharpy: |
        it = iter([1, 2])
        next(it, -1)     # 1
        next(it, -1)     # 2
        next(it, -1)     # -1 (default — no exception)

  - id: iterator-repr-no-address
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "Most iterator `repr`s embed the object's memory address — `<map object at 0x10372d4e0>` — and name the iterator after the container that produced it (`iter([1,2])` is a `list_iterator`, `iter({1})` a `set_iterator`, `iter('ab')` a `str_ascii_iterator`)."
    sharpy_behavior: "Iterator `repr`s are stable and address-free — `<map object>` — so they can be pinned by a test. Iterators built by `iter()` render the generic `<iterator object>` rather than naming their source container. `range` is NOT affected: CPython's `range` repr carries no address and Sharpy matches it exactly, including omitting the step when it is 1."
    spec_ref: docs/language_specification/builtin_functions.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        print(map(abs, [1]))        # <map object at 0x10372d4e0>
        print(reversed([1, 2]))     # <list_reverseiterator object at 0x10372d420>
        print(iter([1, 2]))         # <list_iterator object at 0x10372d4e0>
        print(range(3))             # range(0, 3)      — no address, matched exactly
        print(range(1, 5, 1))       # range(1, 5)      — step omitted when 1
      sharpy: |
        print(map(abs, [1]))        # <map object>
        print(reversed([1, 2]))     # <list_reverseiterator object>
        print(iter([1, 2]))         # <iterator object>   — source container not named
        print(range(3))             # range(0, 3)
        print(range(1, 5, 1))       # range(1, 5)

  - id: dict-views-are-lists
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "`keys()`, `values()` and `items()` on a mapping return LIVE VIEW objects (`dict_keys`, `dict_values`, `dict_items`). A view reflects later mutations of the mapping, and does not support `append`."
    sharpy_behavior: "They return a list copy taken at the call. Later mutations of the mapping are not reflected, and the copy has a list's full surface, so `append` succeeds and affects only the copy. Call them — `c.keys()`; the bare `c.keys` is the method itself, not a value, matching CPython."
    spec_ref: docs/language_specification/dotnet_interop.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        d = {"a": 1}
        k = d.keys()
        d["b"] = 2
        print(list(k))   # ['a', 'b'] — the view saw the new key
      sharpy: |
        c: Counter[str] = Counter[str](["a"])
        k = c.keys()
        c.update(["b"])
        print(len(k))    # 1 — the copy did not see the new key

  - id: no-generator-send-throw
    code: null
    category: semantics
    audience: python
    severity: none
    python_behavior: "Generators support `gen.send(value)`, `gen.throw(exc)`, and `gen.close()` for bidirectional coroutine-style communication."
    sharpy_behavior: "Generators emit `IEnumerable<T>` / `IAsyncEnumerable<T>`. `send()`, `throw()`, and `close()` are not supported. Use async/await for cooperative communication."
    spec_ref: docs/language_specification/generators.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        def gen():
            x = yield 1
            yield x + 1
        g = gen()
        next(g)
        g.send(10)        # yields 11
      sharpy: |
        # No equivalent. Use 'async def' + IAsyncEnumerable, or a class
        # with an explicit channel/queue for bidirectional communication.

  - id: match-statement-differences
    code: null
    category: semantics
    audience: python
    severity: none
    python_behavior: "PEP 634 structural pattern matching — class patterns use `__match_args__`, capture vs. constant disambiguated by name shape (dotted = constant)."
    sharpy_behavior: "`match` exists but with C#-style pattern semantics: tagged-union case patterns, type patterns (`case T(...)`), property patterns, and exhaustiveness checking. Constant pattern shadowing is a warning (SPY0468)."
    spec_ref: docs/language_specification/match_statement.md
    existing_diagnostic: SPY0468
    planned_diagnostic: null
    example:
      python: |
        match cmd:
            case Move(x, y):
                ...
            case Quit():
                ...
      sharpy: |
        match cmd:
            case Command.Move(x, y):
                ...
            case Command.Quit():
                ...
        # Match expressions require exhaustiveness — SPY0416 / SPY0463

  - id: no-variable-arity-tuple-from-iterable
    code: SPY0338
    category: types
    audience: python
    severity: error
    python_behavior: "`tuple(iterable)` builds a tuple as long as the iterable; arity is a runtime property."
    sharpy_behavior: "`tuple(iterable)` is rejected. A tuple's arity is part of its type (`tuple[int, str]` lowers to a .NET ValueTuple), so a runtime-length value has no tuple type. Use `list(...)` for a runtime-length sequence, or a tuple literal when the arity is known. Tuple literals, explicit `tuple[...]` type arguments, and tuple unpacking are unaffected."
    spec_ref: docs/language_specification/tuple_unpacking.md
    existing_diagnostic: SPY0338
    planned_diagnostic: null
    example:
      python: |
        xs = [1, 2, 3]
        t = tuple(xs)     # (1, 2, 3)
      sharpy: |
        xs: list[int] = [1, 2, 3]
        t = tuple(xs)     # ERROR SPY0338: variable-length tuple(iterable) is not supported
        t2: tuple[int, int, int] = (xs[0], xs[1], xs[2])

  - id: no-namedtuple-runtime
    code: SPY0432
    category: stdlib
    audience: python
    severity: error
    python_behavior: "`collections.namedtuple` and `typing.NamedTuple` create lightweight tuple subclasses with named fields."
    sharpy_behavior: "Runtime `namedtuple` is rejected. Use a `@dataclass`, a `struct`, or named tuple types (`tuple[x: int, y: int]`)."
    spec_ref: docs/language_specification/named_tuples.md
    existing_diagnostic: SPY0432
    planned_diagnostic: null
    example:
      python: |
        from collections import namedtuple
        Point = namedtuple("Point", "x y")
      sharpy: |
        @dataclass
        class Point:
            x: int
            y: int

  - id: mutable-default-rejected
    code: SPY0400
    category: semantics
    audience: python
    severity: error
    python_behavior: "Mutable default arguments are evaluated once and shared across calls — a classic foot-gun."
    sharpy_behavior: "Mutable default arguments are rejected at compile time. Use `None` and instantiate inside the body."
    spec_ref: docs/language_specification/function_default_parameters.md
    existing_diagnostic: SPY0400
    planned_diagnostic: null
    example:
      python: |
        def f(xs=[]):  # shared across calls!
            xs.append(1)
            return xs
      sharpy: |
        def f(xs: list[int]? = None) -> list[int]:
            xs = xs or []
            xs.append(1)
            return xs

  - id: identity-with-value-types
    code: SPY0465
    category: operators
    audience: python
    severity: warning
    python_behavior: "`is`/`is not` compare object identity. For small ints and interned strings the result is unspecified-but-often-True."
    sharpy_behavior: "`is`/`is not` lower to reference equality. Using them on value types (struct, primitives) is a warning — they always compare boxed identity. Axiom precedence: .NET (1) > Python (2)."
    spec_ref: docs/language_specification/identity_operators.md
    existing_diagnostic: SPY0465
    planned_diagnostic: null
    example:
      python: |
        if x is 1:    # SyntaxWarning in 3.8+
            ...
      sharpy: |
        if x is 1:    # SPY0465: 'is' with value types is unreliable
            ...

  - id: dunder-direct-invocation
    code: SPY0427
    category: semantics
    audience: python
    severity: error
    python_behavior: "Calling `obj.__add__(other)` directly is permitted (though uncommon)."
    sharpy_behavior: "Direct dunder invocation is rejected. Use the corresponding operator or builtin (`a + b`, `len(x)`, `str(x)`, ...)."
    spec_ref: docs/language_specification/dunder_invocation_rules.md
    existing_diagnostic: SPY0427
    planned_diagnostic: null
    example:
      python: |
        a.__add__(b)
      sharpy: |
        a + b   # use the operator instead

  - id: implicit-bool-conversion
    code: SPY0241
    category: types
    audience: python
    severity: error
    python_behavior: "Any value can be used as a condition; `__bool__`/`__len__` provide truthiness."
    sharpy_behavior: "Conditions must have type `bool`. Implement `__bool__` (synthesizes `IBoolConvertible`) for explicit `bool(x)` conversion."
    spec_ref: docs/language_specification/boolean_literals.md
    existing_diagnostic: SPY0241
    planned_diagnostic: null
    example:
      python: |
        if my_list:
            ...
      sharpy: |
        if len(my_list) > 0:
            ...
        # Or implement __bool__ and call bool(my_list).

  - id: comparison-chaining
    code: null
    category: operators
    audience: csharp
    severity: none
    python_behavior: "`a < b < c` chains as `a < b and b < c` with `b` evaluated once."
    sharpy_behavior: "Same as Python — comparison chaining is supported (a deliberate Python-favoring exception to the C#-first rule)."
    spec_ref: docs/language_specification/comparison_chaining.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        if 0 < x < 10: ...
      sharpy: |
        if 0 < x < 10: ...   # supported

  - id: walrus-no-annotation
    code: null
    category: syntax
    audience: csharp
    severity: none
    python_behavior: "Python's walrus has never accepted a type annotation."
    sharpy_behavior: "Walrus does not accept a type annotation; the type is always inferred from the RHS."
    spec_ref: docs/language_specification/walrus_operator.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        if (x := compute()) > 0: ...
      sharpy: |
        if (x := compute()) > 0: ...

  - id: module-level-executable-statement
    code: SPY0340
    category: semantics
    audience: python
    severity: error
    python_behavior: "Arbitrary statements at module top level execute on import."
    sharpy_behavior: "Module top level allows only declarations, imports, type aliases, and constant initializers. Executable code lives in `main()`."
    spec_ref: docs/language_specification/program_entry_point.md
    existing_diagnostic: SPY0340
    planned_diagnostic: null
    example:
      python: |
        print("hello")    # runs on import
      sharpy: |
        def main() -> None:
            print("hello")

  - id: missing-main-function
    code: SPY0403
    category: semantics
    audience: python
    severity: error
    python_behavior: "Any script can run; an explicit `if __name__ == \"__main__\":` block is conventional but optional."
    sharpy_behavior: "Executable programs require a top-level `def main() -> None:` function."
    spec_ref: docs/language_specification/program_entry_point.md
    existing_diagnostic: SPY0403
    planned_diagnostic: null
    example:
      python: |
        # script.py
        do_work()
      sharpy: |
        def main() -> None:
            do_work()

  - id: no-multiple-ways-string-formatting
    code: null
    category: syntax
    audience: python
    severity: none
    python_behavior: "Multiple equivalent options: `%`-formatting, `str.format`, f-strings, `Template`."
    sharpy_behavior: "F-strings and template strings (t-strings) are the canonical forms. `%`-formatting and `str.format` are not encouraged but `format` extension exists."
    spec_ref: docs/language_specification/fstrings.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        "%s is %d" % (name, age)
        "{} is {}".format(name, age)
        f"{name} is {age}"
      sharpy: |
        f"{name} is {age}"     # canonical

  - id: type-narrowing-or-not-supported
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "Some type checkers narrow with `or` (e.g., `isinstance(x, int) or isinstance(x, str)` narrows to `int | str`)."
    sharpy_behavior: "`or` does not produce union types. Type narrowing only happens on `is None`/`is not None` and single `isinstance(x, T)` checks."
    spec_ref: docs/language_specification/type_narrowing.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        if isinstance(x, int) or isinstance(x, str):
            x.bit_length()    # type checker may narrow to int|str
      sharpy: |
        if isinstance(x, int):
            x.bit_length()    # narrowed to int here
        elif isinstance(x, str):
            x.upper()         # narrowed to str here

  - id: variance-required-on-generics
    code: SPY0418
    category: types
    audience: python
    severity: error
    python_behavior: "Variance is computed by tools like mypy from `_co`/`_contra` TypeVars; runtime is invariant."
    sharpy_behavior: "Generic variance is explicit (`out T`, `in T`) on type parameters and validated at compile time. Mismatched positions are errors (SPY0418/SPY0419)."
    spec_ref: docs/language_specification/generic_variance.md
    existing_diagnostic: SPY0418
    planned_diagnostic: null
    example:
      python: |
        T_co = TypeVar("T_co", covariant=True)
        class Box(Generic[T_co]): ...
      sharpy: |
        class Box[out T]:
            def get(self) -> T: ...

  - id: enum-value-type-consistency
    code: SPY0253
    category: types
    audience: python
    severity: error
    python_behavior: "`enum.Enum` members can have arbitrary mixed-type values."
    sharpy_behavior: "All members of an enum must share the same value type."
    spec_ref: docs/language_specification/enums.md
    existing_diagnostic: SPY0253
    planned_diagnostic: null
    example:
      python: |
        class Mixed(Enum):
            A = 1
            B = "two"     # OK in Python
      sharpy: |
        enum Mixed:
            A = 1
            B = "two"     # ERROR SPY0253: inconsistent enum value types

  - id: dataclass-field-ordering
    code: SPY0381
    category: syntax
    audience: python
    severity: error
    python_behavior: "Python dataclass requires fields without defaults before fields with defaults."
    sharpy_behavior: "Same rule, enforced at compile time (SPY0381) — also enforced for plain structs (SPY0435)."
    spec_ref: docs/language_specification/dataclass.md
    existing_diagnostic: SPY0381
    planned_diagnostic: null
    example:
      python: |
        @dataclass
        class C:
            a: int = 0
            b: int     # TypeError at class creation
      sharpy: |
        @dataclass
        class C:
            b: int
            a: int = 0   # OK

  - id: late-bound-default-self-reference
    code: SPY0433
    category: semantics
    audience: python
    severity: error
    python_behavior: "Default-argument expressions are evaluated once at definition and cannot reference `self`."
    sharpy_behavior: "Same — referencing `self` in a default value is a compile-time error."
    spec_ref: docs/language_specification/function_default_parameters.md
    existing_diagnostic: SPY0433
    planned_diagnostic: null
    example:
      python: |
        class C:
            def f(self, x=self.value):  # NameError
                ...
      sharpy: |
        class C:
            def f(self, x: int = self.value):
                # ERROR SPY0433: cannot reference 'self' in default value
                ...

  - id: implicit-protocol-synthesis
    code: SPY1001
    category: types
    audience: csharp
    severity: info
    python_behavior: "Python uses structural duck typing for `len`, `bool`, etc."
    sharpy_behavior: "Defining `__len__` / `__bool__` / `__reversed__` causes the emitter to implicitly add `ISized` / `IBoolConvertible` / `IReverseEnumerable[T]` to the class's interface list (SPY1001 info)."
    spec_ref: docs/language_specification/dunder_methods.md
    existing_diagnostic: SPY1001
    planned_diagnostic: null
    example:
      python: |
        class Bag:
            def __len__(self): return 0
      sharpy: |
        class Bag:
            def __len__(self) -> int:
                return 0
        # Note SPY1001: Bag now implicitly implements ISized.

  - id: unknown-dunder-rejected
    code: SPY0414
    category: semantics
    audience: python
    severity: error
    python_behavior: "Any `__name__` is permitted; unknown dunders are inert."
    sharpy_behavior: "Only the documented dunder set is allowed. Unknown dunders are rejected at compile time (SPY0414)."
    spec_ref: docs/language_specification/dunder_methods.md
    existing_diagnostic: SPY0414
    planned_diagnostic: null
    example:
      python: |
        class C:
            def __wibble__(self): ...   # Python allows
      sharpy: |
        class C:
            def __wibble__(self) -> None: ...
            # ERROR SPY0414: unknown dunder method

  - id: naming-conventions-enforced
    code: SPY0453
    category: syntax
    audience: python
    severity: warning
    python_behavior: "PEP 8 conventions are advisory; the runtime accepts any valid identifier casing."
    sharpy_behavior: "Naming conventions are enforced as warnings (SPY0453): types `PascalCase`, functions/variables `snake_case`, constants `SCREAMING_SNAKE_CASE`."
    spec_ref: docs/language_specification/naming_conventions.md
    existing_diagnostic: SPY0453
    planned_diagnostic: null
    example:
      python: |
        my_class = type("Foo", (), {})  # any naming OK
      sharpy: |
        class my_class: ...   # WARNING SPY0453: should be PascalCase

  - id: bodyless-method-syntax-deprecated
    code: SPY0464
    category: syntax
    audience: python
    severity: warning
    python_behavior: "N/A — Python has no body-less method syntax outside `Protocol` ellipsis."
    sharpy_behavior: "Body-less method syntax (`def foo(self) -> int` without a body) is deprecated (SPY0464). Use `def foo(self) -> int: ...` for abstract/interface methods."
    spec_ref: docs/language_specification/dunder_invocation_rules.md
    existing_diagnostic: SPY0464
    planned_diagnostic: null
    example:
      python: |
        # not applicable
        pass
      sharpy: |
        class IFoo:
            def bar(self) -> int      # WARNING SPY0464: deprecated
            def baz(self) -> int: ... # OK — preferred

  - id: int-overflow-checked
    code: null
    category: semantics
    audience: python
    severity: none
    python_behavior: "Python `int` is arbitrary precision; no overflow. `2 ** 40` is exact."
    sharpy_behavior: "`int` maps to `System.Int32` (32-bit). `+`/`-`/`*` with **variable** operands follow .NET semantics and wrap under the default unchecked context; the same operators with **constant** operands are refused at compile time with SPY0348 (#1234), mirroring C#'s own split (CS0220 is an error while the runtime wraps). `**` never wraps either way, but reaches that by a different route: with a variable exponent it routes through `Sharpy.Builtins.CheckedIntPow` (#905) and raises `OverflowError` rather than saturating through a lossy `Math.Pow` round-trip, while a constant `**` widens `int` to `long` and is refused with SPY0328 only past 64 bits. That widening is why `2 ** 40` is accepted while the smaller-valued `3794 * 1973 * 948` is refused — an asymmetry tracked in #1316. Use `long` for 64-bit. Axiom precedence: .NET (1) > Python (2)."
    spec_ref: docs/language_specification/primitive_types.md
    existing_diagnostic: SPY0348
    planned_diagnostic: null
    example:
      python: |
        2 ** 40             # 1099511627776, exact
        2147483647 + 1      # 2147483648, exact
      sharpy: |
        # int is 32-bit. Every line below was executed against the compiler.
        print(2147483647 + 1)   # SPY0348 — constant overflow is refused, not wrapped
        print(n + 1)            # n: int = 2147483647 -> -2147483648 (runtime wraps)
        print(2 ** 40)          # 1099511627776 — a constant '**' widens int -> long
        print(2 ** e)           # e: int = 40 -> raises OverflowError (checked)
        print(2 ** 100)         # SPY0328 — past 64 bits, refused
        x: long = 1L << 62      # use 'long' for 64-bit (a bare 1 << 62 is SPY0348 — #1315)

  - id: shift-count-masked
    code: null
    category: operators
    audience: python
    severity: none
    python_behavior: "Python integers are arbitrary precision and the shift count is used as written: `1 << 40` is `1099511627776`, `1 << 62` is `4611686018427387904`. A negative count raises `ValueError: negative shift count` (verified with python3)."
    sharpy_behavior: "A shift emits the .NET operator, which **masks the count** to the left operand's width — 5 bits for `int`, 6 for `long`. With a runtime count, `n << 40` on an `int` computes `n << 8`. Sharpy closes the compile-time half of the hole: a CONSTANT shift whose exact value does not fit the expression's width is refused with SPY0348, and a CONSTANT negative count is refused with SPY0213 (#1315), so the silent cases that remain are exactly those where the count is only known at runtime. The result type follows the left operand alone, so `1L << 62` is the 64-bit spelling — annotating the destination variable does not widen the shift. Axiom precedence: .NET (1) > Python (2)."
    spec_ref: docs/language_specification/bitwise_operators.md
    existing_diagnostic: SPY0348
    planned_diagnostic: null
    example:
      python: |
        print(1 << 40)          # 1099511627776, exact
        print(1 << -1)          # ValueError: negative shift count
      sharpy: |
        # Every line below was executed against the compiler.
        print(1 << 40)          # SPY0348 — the constant does not fit int, refused
        print(1L << 40)         # 1099511627776 — a long left operand
        print(n << s)           # n: int = 1, s: int = 40 -> 256 (count masks to 8)
        print(1 << -1)          # SPY0213 — a constant negative count is refused
        print(1 << k)           # k: int = -1 -> -2147483648 (runtime count, masks to 31)

  - id: int-power-negative-exponent
    code: null
    category: semantics
    audience: python
    severity: none
    python_behavior: "`int ** int` with a negative exponent returns a **float**: `2 ** -1` is `0.5`, `1 ** -5` is `1.0`, `(-1) ** -3` is `-1.0`, `10 ** -2` is `0.01` (verified with python3)."
    sharpy_behavior: "`int ** int` is typed `int` regardless of the exponent's sign, so a negative exponent takes the truncating double path and the fractional result truncates toward zero: `2 ** -1` is `0`. Changing the result type on the *sign of a runtime value* would make the expression's static type depend on data, which Axiom 3 forbids; use a float base (`2.0 ** -1`) to get `0.5`. Axiom precedence: type safety (3) > Python (2)."
    spec_ref: docs/language_specification/arithmetic_operators.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        2 ** -1             # 0.5 (float)
        1 ** -5             # 1.0 (float)
      sharpy: |
        print(2 ** -1)      # 0    — int ** int stays int; truncated
        print(2.0 ** -1)    # 0.5  — float base gives the Python answer

  - id: decimal-float-comparison-refused
    code: SPY0222
    category: operators
    audience: python
    severity: error
    python_behavior: "`Decimal` compares against `float` directly: `Decimal(1) < 2.0` is `True` (verified with python3). CPython converts the float to its exact decimal value for the comparison, so the two types mix in every comparison operator."
    sharpy_behavior: "A `decimal` operand against a `float64` operand is refused with SPY0222 — `Type 'decimal' does not support operator '<' with operand of type 'float64'`. C# has no implicit conversion in either direction (`decimal` has more precision, `double` more range), so the pair does not bind under the binary numeric promotion the comparison operators share with the arithmetic ones (C# §12.4.7). Axiom precedence: .NET (1) > Python (2). Convert explicitly to name the comparison type: `d < decimal(f)` or `float(d) < f`. `decimal` against an *integer* is unaffected (`decimal < int` promotes the integer and runs)."
    spec_ref: docs/language_specification/comparison_operators.md
    existing_diagnostic: SPY0222
    planned_diagnostic: null
    example:
      python: |
        from decimal import Decimal
        print(Decimal(1) < 2.0)     # True
      sharpy: |
        d: decimal = 1
        f: float64 = 2.0
        # r: bool = d < f           # ERROR (SPY0222)
        print(d < decimal(f))       # True  — compare as decimal
        print(float(d) < f)         # True  — or compare as float64

  - id: float-is-double
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "`float` is IEEE-754 double (64-bit)."
    sharpy_behavior: "`float` maps to `System.Double` (64-bit) — same precision as Python. `float32` is the 32-bit alternative."
    spec_ref: docs/language_specification/primitive_types.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        x: float = 1.0
      sharpy: |
        x: float = 1.0       # 64-bit
        y: float32 = 1.0f    # 32-bit

  - id: numeric-widening-in-typed-context
    code: null
    category: types
    audience: python
    severity: none
    python_behavior: "A container holds the objects put into it. `{\"a\": 5, \"b\": 2.5}` keeps the `int` 5, and `print(d[\"a\"])` prints `5`."
    sharpy_behavior: "A contextual type (annotation or declared return type) closes the element type, so an `int` element of a `dict[str, float]` / `list[float]` is widened to `double` at compile time and prints as `5.0`. There is no runtime int-in-a-float-slot: the alternative is an `object`/union element type, which forfeits static typing. Axiom precedence: types (3) > Python (2)."
    spec_ref: docs/language_specification/collection_types.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        d = {"a": 5, "b": 2.5}
        print(d["a"])        # 5
      sharpy: |
        d: dict[str, float] = {"a": 5, "b": 2.5}
        print(d["a"])        # 5.0

  - id: min-max-mixed-numeric-promotion
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "`min`/`max` return the winning element unchanged, so `min(2, 3.0)` is the `int` 2."
    sharpy_behavior: "The variadic value form of `min`/`max` promotes mixed-numeric arguments to a common numeric type (#1014), so `min(2, 3.0)` is `2.0` (float64) and the result composes with arithmetic and annotations. Returning the unpromoted element would require an `object`/union return type. This matches Sharpy's binary numeric operators (`2 + 0.0 == 2.0`). Axiom precedence: types (3) > Python (2)."
    spec_ref: docs/language_specification/builtin_functions.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        print(min(2, 3.0))   # 2
      sharpy: |
        print(min(2, 3.0))   # 2.0

  - id: math-floor-ceil-return-float
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "`math.floor`/`math.ceil` return `int`, so `math.floor(3.7)` is `3` and prints `3`."
    sharpy_behavior: "They return `float` (double), so `math.floor(3.7)` is `3.0`. Returning `int` would overflow above 2^31 — `math.floor(1e10)` would raise where CPython gives 10000000000 — and returning `long` would still diverge above 2^63 while breaking every caller typed `int` or `float`; parity here has a real cost, which is when the codified precedence (.NET > Types > Python) applies, unlike `//` and `%` where Python semantics were adopted at zero cost. Sharpy already tells this story at the operator: `7.0 // 2.0` is `3.0`, a float — and CPython agrees there, so it is CPython that is inconsistent between its operator and its function. `int(math.floor(x))` is the explicit bridge (#1350)."
    spec_ref: docs/stdlib/math.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        import math
        print(math.floor(3.7))        # 3
        print(math.ceil(3.7))         # 4
        print(math.floor(-3.2))       # -4
        print(7.0 // 2.0)             # 3.0   (a float here too)
        print(int(math.floor(3.7)))   # 3     (the explicit bridge)
      sharpy: |
        import math
        print(math.floor(3.7))        # 3.0
        print(math.ceil(3.7))         # 4.0
        print(math.floor(-3.2))       # -4.0
        print(7.0 // 2.0)             # 3.0
        print(int(math.floor(3.7)))   # 3

  - id: ellipsis-body-raises
    code: null
    category: semantics
    audience: python
    severity: none
    python_behavior: "`...` is an ordinary expression statement (the `Ellipsis` singleton). A function whose whole body is `...` runs fine and returns `None`."
    sharpy_behavior: "`...` as a concrete function or method body lowers to `throw new NotImplementedException()`. Returning `None` from a body annotated `-> int` is not expressible, so the placeholder must fail loudly. In an abstract method or a body-less interface member it is a no-op instead. Axiom precedence: .NET (1) and types (3) > Python (2)."
    spec_ref: docs/language_specification/ellipsis_literal.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        def todo() -> int: ...
        print(todo())        # None
      sharpy: |
        def todo() -> int: ...
        print(todo())        # raises NotImplementedError

  - id: super-requires-class-context
    code: SPY0284
    category: semantics
    audience: python
    severity: error
    python_behavior: "`super()` is a class-aware reference resolved at call time."
    sharpy_behavior: "`super().method(...)` is allowed only inside instance methods of a class with a base class (SPY0284/SPY0285)."
    spec_ref: docs/language_specification/inheritance.md
    existing_diagnostic: SPY0284
    planned_diagnostic: null
    example:
      python: |
        super().__init__()    # in any method
      sharpy: |
        class B(A):
            def __init__(self) -> None:
                super().__init__()   # OK

  - id: pow-returns-float
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "`pow(int, int)` with a non-negative exponent returns an **int**: `pow(2, 40)` is `1099511627776`, `pow(2, 3)` is `8` (verified with python3 3.12)."
    sharpy_behavior: "`pow(x, y)` maps to `Math.Pow`, which returns `double`, so an all-integer call prints with a `.0`: `pow(2, 40)` is `1099511627776.0` and `pow(2, 3)` is `8.0`. Intentional and specified — `builtin_functions.md` documents the `Math.Pow` mapping and `Sharpy.Core/Pow.cs` declares `double Pow(int, int)` — but silent and user-visible, which is why it is catalogued (#1317). The `**` OPERATOR is a different rule with its own entry (`int-power-negative-exponent`): `2 ** 3` is the int `8`. Use `**` when you want Python's integer result."
    spec_ref: docs/language_specification/builtin_functions.md
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        print(pow(2, 40))   # 1099511627776
        print(pow(2, 3))    # 8
      sharpy: |
        print(pow(2, 40))   # 1099511627776.0
        print(pow(2, 3))    # 8.0
        print(2 ** 3)       # 8    — the operator stays int

  - id: no-implicit-string-concat
    code: SPY0103
    category: syntax
    audience: python
    severity: error
    python_behavior: "Adjacent string literals are joined at parse time: `\"hello \" \"world\"` is the single string `hello world`. The join is invisible and applies across line breaks inside brackets."
    sharpy_behavior: "Adjacent string literals are a parse error (SPY0103, 'Expected end of statement, got String'). Refused deliberately, not unimplemented (#1269): the feature's cost is the missing-comma footgun — a dropped comma in a list of strings silently concatenates two elements into one instead of failing, and the resulting list is short by one with no diagnostic anywhere. Use `+` for an explicit join, or an f-string when interpolating. Axiom precedence: type safety (3) > Python (2)."
    spec_ref: docs/language_specification/string_literals.md
    existing_diagnostic: SPY0103
    planned_diagnostic: null
    example:
      python: |
        s = "hello " "world"        # 'hello world'
        xs = ["a", "b" "c"]         # ['a', 'bc'] — the missing comma is silent
      sharpy: |
        # ERROR SPY0103: Expected end of statement, got String
        # s: str = "hello " "world"
        s: str = "hello " + "world"     # explicit
        name: str = "world"
        greeting: str = f"hello {name}"  # interpolating

  - id: paramspec-unsupported
    code: SPY0310
    category: types
    audience: python
    severity: error
    python_behavior: "`typing.ParamSpec` captures a callable's whole parameter list so a decorator can be typed as preserving it: `Callable[P, T] -> Callable[P, T]`."
    sharpy_behavior: "Not supported, closed as not planned (#996). Importing `typing` is itself refused (SPY0310) — Sharpy's type constructs are native syntax, and there is no ParamSpec among them. A signature-preserving wrapper is written with an explicit delegate type or generic parameters; Sharpy decorators are compile-time and do not re-type the function they decorate, so the problem ParamSpec solves does not arise in the same form."
    spec_ref: docs/language_specification/generics.md
    existing_diagnostic: SPY0310
    planned_diagnostic: null
    example:
      python: |
        from typing import ParamSpec, TypeVar, Callable
        P = ParamSpec("P")
        R = TypeVar("R")
        def log(f: Callable[P, R]) -> Callable[P, R]: ...
      sharpy: |
        # ERROR SPY0310: The 'typing' module is not needed in Sharpy.
        # A concrete callable type states the signature it preserves:
        def apply(f: (int) -> int, x: int) -> int:
            return f(x)

  - id: typevartuple-unsupported
    code: SPY0310
    category: types
    audience: python
    severity: error
    python_behavior: "`typing.TypeVarTuple` is a variadic type variable, so a generic can be parameterised by an arbitrary-length sequence of types (`Array[*Ts]`)."
    sharpy_behavior: "Not supported, closed as not planned (#997). Importing `typing` is refused (SPY0310), and variadic generics have no native spelling: .NET generics are fixed-arity, so a variadic type parameter has nothing to lower to (Axiom 1). Use a fixed arity, a tuple type, or a common interface."
    spec_ref: docs/language_specification/generics.md
    existing_diagnostic: SPY0310
    planned_diagnostic: null
    example:
      python: |
        from typing import TypeVarTuple, Generic
        Ts = TypeVarTuple("Ts")
        class Array(Generic[*Ts]): ...
      sharpy: |
        # ERROR SPY0310: The 'typing' module is not needed in Sharpy.
        # Fixed arity, or a tuple when the shape is the point:
        def pair_of(a: int, b: str) -> tuple[int, str]:
            return (a, b)

  - id: star-import-builtin-rebinding
    code: SPY0492
    category: scoping
    audience: python
    severity: error
    python_behavior: "`from M import *` silently rebinds any name it supplies, including builtins. With a module exporting `len`, `from shadowlib import *` then `len([1,2,3])` prints `999`, not `3` — verified with python3. Nothing is reported at the import or at the use."
    sharpy_behavior: "The star-import is accepted, but an unqualified USE of a name it displaced from the builtin namespace is refused with SPY0492. Sharpy is deliberately stricter than CPython here and follows C# instead: CS0104 lets two `using` directives supply the same name and refuses only the ambiguous reference, which is the same rule scoped to the same place. The star-import itself stays legal, so `from numpy import *` in a file that never calls `sum` compiles. An EXPLICIT `from M import len` is the statement of intent that resolves the ambiguity — it is honoured, and warns (SPY0484) in the file where the rebinding takes effect, since the SPY0483 declaration-site warning lives in the library's file. Axiom precedence: type safety (3) > Python (2)."
    spec_ref: docs/language_specification/imports.md
    existing_diagnostic: SPY0492
    planned_diagnostic: null
    example:
      python: |
        # shadowlib.py exports `def len(x): return 999`
        from shadowlib import *
        print(len([1, 2, 3]))   # 999 — silently the library's
      sharpy: |
        # Every line below was executed against the compiler.
        from shadowlib import *
        print(len(xs))          # SPY0492 — ambiguous, refused at the USE
        # Say which one you mean:
        from shadowlib import len   # warning SPY0484, then 999
        import builtins
        print(builtins.len(xs))     # 3 — 'import builtins' is required

  - id: mixed-set-frozenset-equality
    code: SPY0222
    category: operators
    audience: python
    severity: error
    python_behavior: "A set and a frozenset compare by their elements, so the two types are interchangeable operands for every set operator including equality: `{1} == frozenset([1])` is True and `{1} != frozenset([1])` is False — verified with python3."
    sharpy_behavior: "The four set operations (`|`, `&`, `-`, `^`) and the four subset/superset comparisons DO accept mixed `set`/`frozenset` operands in both directions, following CPython's left-operand rule — the left operand's type decides the result (#1312). Mixed `==`/`!=` alone is refused, SPY0222, in both directions. The reason is measured rather than an oversight: equality operators take NULLABLE operands, so declaring `operator ==(Set<T>?, FrozenSet<T>?)` alongside the existing `operator ==(Set<T>?, Set<T>?)` makes the ordinary `someSet == None` ambiguous — the null literal converts to both and C# reports CS9342. That is not hypothetical; it broke Sharpy.Core's own DictKeyView on the first build, and it would break the same shape in every user program, which is worse than the gap it closes. The eight operators that do take mixed operands have non-nullable parameters and carry no such conflict, which is exactly why equality is the only missing cell. Converting one side states which comparison is meant and both spellings hold. Axiom precedence: .NET (1) > Python (2)."
    spec_ref: docs/language_specification/collection_types.md
    existing_diagnostic: SPY0222
    planned_diagnostic: null
    example:
      python: |
        {1} == frozenset([1])       # True
        {1} != frozenset([1])       # False
      sharpy: |
        # Every line below was executed against the compiler.
        s: set[int] = {1}
        f: frozenset[int] = frozenset([1])
        print(s == f)               # ERROR SPY0222: Type 'set[int]' does not support
                                    # operator '==' with operand of type 'frozenset[int]'
        print(f == s)               # ERROR SPY0222 — the refusal is symmetric
        print(s | f)                # {1} — the set OPERATIONS do take mixed operands
        print(s == set(f))          # True — convert one side to say what you mean
        print(frozenset(s) == f)    # True

  - id: np-split-nonpositive-sections-valueerror
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "numpy raises `ZeroDivisionError: integer modulo by zero` for `np.split(a, 0)` and `ValueError: number sections must be larger than 0.` for a negative sections count — the ZeroDivisionError is an artifact of numpy computing `N % sections` before validating the argument (measured against numpy 2.5.1)."
    sharpy_behavior: "Both non-positive sections counts raise the ValueError, with numpy's own message. The ZeroDivisionError is an implementation accident, not a contract worth reproducing; unifying on the validated error is deliberate (recorded in 3c4db7803 at the call site: \"Say so if you want it faithful instead\")."
    spec_ref: null
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        np.split(np.arange(6.0), 0)    # ZeroDivisionError: integer modulo by zero
        np.split(np.arange(6.0), -1)   # ValueError: number sections must be larger than 0.
      sharpy: |
        np.split(a6, 0)                # ValueError: number sections must be larger than 0.
        np.split(a6, -1)               # ValueError: number sections must be larger than 0.

  - id: json-lone-surrogate-escapes-refused
    code: null
    category: stdlib
    audience: python
    severity: none
    python_behavior: "CPython's `json.loads` accepts unpaired UTF-16 surrogate escapes (`\\ud800`) and produces a string containing the lone surrogate code unit. The resulting string explodes on its first `encode('utf-8')` with `UnicodeEncodeError: 'utf-8' codec can't encode character '\\ud800'` (measured: python3 3.12.13, `json.loads('\"\\ud800\"')` returns a 1-char string, `.encode('utf-8')` raises)."
    sharpy_behavior: "Both `json.loads` (untyped) and `json.loads[T]` (typed) refuse unpaired surrogate escapes at parse time with `JSONDecodeError('Unpaired UTF-16 surrogate escape \\ud800')`. The refusal is Axiom 1 (.NET-first): .NET strings are validated UTF-16 and lone surrogates are a deferred trap. One shared scanner (`JsonSurrogateEscapes`) serves both doors so they refuse the same documents with the same message (#1487, owner ruling 2026-08-13)."
    spec_ref: null
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        import json
        s = json.loads('"\\ud800"')    # returns '\ud800' (lone surrogate)
        s.encode('utf-8')              # UnicodeEncodeError
      sharpy: |
        import json
        json.loads('"\\ud800"')        # JSONDecodeError: Unpaired UTF-16 surrogate escape \ud800

  - id: cross-type-numeric-hash
    code: null
    category: semantics
    audience: python
    severity: none
    python_behavior: "CPython's numeric tower guarantees that equal numbers hash equally across types: `hash(3) == hash(3.0) == hash(complex(3,0)) == hash(Decimal(3)) == 3` (python3 3.12, measured). This extends to `hash(True) == hash(1)` and large integers: `hash(2**62) == 2`."
    sharpy_behavior: |
      Sharpy's int/float/complex are CLR primitives whose GetHashCode cannot be overridden
      without wrapper types (Axiom 1 violation, anti-pattern). Same-type hashing is consistent;
      cross-type pairs diverge. Measured matrix at 8e1d6b5af:

        hash(3)                     = 3      (agrees with CPython)
        hash(True) == hash(1)       = True   (agrees)
        hash(small_long) == hash(3) = True   (agrees; long(3))
        hash(decimal(3)) == hash(3) = True   (agrees)
        hash(3.0)                   = 1074266112  (CPython: 3)
        hash(3.0) == hash(3)        = False  (CPython: True)
        hash(complex(3,0))          = process-randomized  (CPython: 3, stable)
        hash(complex(3,0)) == hash(3) = False (CPython: True)
        hash(2**62)                 = 1073741824  (CPython: 2)
        hash(-1)                    = -1     (CPython: -2; same-type value divergence)

      hash(complex(...)) is additionally NON-DETERMINISTIC across processes (measured: three
      runs at 8e1d6b5af gave -279744320, -252378607, 1850970276): System.Numerics.Complex
      combines its components with the per-process-seeded HashCode.Combine. CPython's complex
      hash is a stable arithmetic function. Within one process the value is stable, which is
      all dict/set need — but complex hashes must never be persisted or compared across runs.

      Equals/GetHashCode self-consistency is preserved: Complex.Equals(object) only returns true
      for another Complex, and boxed CLR primitives don't cross-type-equal. No dict/set invariant
      breaks. The divergence is observable only as two entries where CPython would have one, in a
      heterogeneous object-keyed container.
    spec_ref: null
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        hash(3.0) == hash(3)            # True
        hash(complex(3, 0)) == hash(3)  # True
        d = {3: "int"}; d[3.0]          # "int" (same slot)
      sharpy: |
        print(hash(3.0) == hash(3))            # False
        print(hash(complex(3, 0)) == hash(3))  # False
        # A dict[object, str] with key 3 and key 3.0 stores two entries, not one

  - id: builtin-exception-python-surface
    code: SPY0215
    category: semantics
    audience: python
    severity: error
    python_behavior: "Builtin exceptions (ValueError, KeyError, TypeError, etc.) have a minimal public surface: `args`, `with_traceback(tb)`, and `add_note(note)` (3.11+). `.message` was removed in Python 3.0. The underlying .NET members (`StackTrace`, `InnerException`, `HelpLink`, `Source`, `Data`, `TargetSite`, `GetBaseException()`) do not exist in CPython."
    sharpy_behavior: "Builtin exception types present Python's exception surface only. Members inherited from System.Exception or other .NET base classes are refused at semantic time with SPY0215 and a steer (`message` steers to `str(e)`). Members the Sharpy class itself declares (ExceptionGroup's `message`/`exceptions`/`subgroup`/`split`/`derive`, SystemExit's `code`) resolve normally. Python's `args`/`with_traceback`/`add_note` are not implemented (a pre-existing gap, not this entry's scope). Imported CLR exception types and user subclasses declaring their own members are unaffected."
    spec_ref: null
    existing_diagnostic: SPY0215
    planned_diagnostic: null
    example:
      python: |
        try:
            raise ValueError("boom")
        except ValueError as e:
            print(e.message)        # AttributeError: 'ValueError' has no attribute 'message'
            print(str(e))           # boom
      sharpy: |
        try:
            raise ValueError("boom")
        except ValueError as e:
            print(e.message)        # SPY0215: not part of the Python exception surface; use str(e)
            print(str(e))           # boom

  - id: type-alias-call-transparency
    code: null
    category: semantic
    audience: python
    severity: none
    python_behavior: "Python 3.12 raises `TypeError: 'typing.TypeAliasType' object is not callable` for `type bint = int; bint(\"42\")`. The PEP 695 `type` statement creates a `TypeAliasType` which is a descriptor, not a callable — it has no `__call__` method."
    sharpy_behavior: "Type aliases are compile-time transparent: `type bint = int; bint(\"42\")` compiles and runs identically to `int(\"42\")`. The alias IS the target type in every position (annotation, call, value, argument). Owner-ruled 2026-08-18 (#1527)."
    spec_ref: "type_aliases.md#transparency"
    existing_diagnostic: null
    planned_diagnostic: null
    example:
      python: |
        type bint = int
        bint("42")     # TypeError: 'typing.TypeAliasType' object is not callable
      sharpy: |
        type bint = int
        v: bint = bint("42")  # compiles, prints 42


  - id: membership-needle-type-mismatch
    code: SPY0222
    category: operators
    audience: python
    severity: error
    python_behavior: "`x in xs` never fails on a type mismatch; a needle no element can equal is simply `False`."
    sharpy_behavior: "The needle is an argument into the container's element slot: a constant converts when in range, a variable C# cannot implicitly convert is refused by name with a cast steer, and a non-numeric mismatch is refused — a value the element type cannot hold can never be a member, so the test is a type error rather than a constant False (plan-757fbb, #1750, R-U)."
    spec_ref: docs/language_specification/membership_operators.md#needle-type
    existing_diagnostic: SPY0222
    planned_diagnostic: null
    example:
      python: |
        xs = [1, 2]
        print("a" in xs)     # False
      sharpy: |
        xs: list[int] = [1, 2]
        print("a" in xs)     # ERROR SPY0222: Type 'str' does not support operator 'in' with operand of type 'list[int32]'
