Skip to content

Make expression fields read-only - #1267

Open
Zeroto521 wants to merge 1 commit into
scipopt:masterfrom
Zeroto521:p3-readonly
Open

Zeroto521 wants to merge 1 commit into
scipopt:masterfrom
Zeroto521:p3-readonly

Conversation

@Zeroto521

@Zeroto521 Zeroto521 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Make the expression classes' internal fields read-only so they cannot be accidentally reassigned or corrupted at runtime, and remove a dead field.

Copilot AI balanced review requested due to automatic review settings October 12, 2026 01:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Zeroto521 Zeroto521 changed the title Make expression fields read-only and remove dead VarExpr.var (P3) Make expression fields read-only and remove dead VarExpr.var Oct 12, 2026
@Zeroto521

Copy link
Copy Markdown
Contributor Author

Notes on the expr.mypy.out baseline changes and related decisions

This PR touches the stub/baseline in ways that may raise questions during review. Three points to explain:

1. Behavior change: Constant(Decimal) now converts to float at construction

Before this PR, cdef number was an untyped object field, so Constant(Decimal("0.1")).number preserved the Decimal. After typing the field cdef readonly double number, the value is converted to float at construction (Constant(Decimal("0.1")).number → 0.1).

This aligns with the existing stub contract: Constant.__init__(self, number: Union[int, float]) never promised Decimal support. Typing the field makes the conversion happen at the entry point instead of failing later with TypeError: unsupported operand type(s) for +=: 'float' and 'decimal.Decimal' during arithmetic. Runtime behavior is consistent with float everywhere; only the timing of the conversion changed.

2. Baseline: net -72 errors vs upstream master, 11 new entries

tests/@types/expr.mypy.out (4530 lines) is regenerated from the branch runtime + new stub. Compared to upstream master (213 unique errors):

  • 83 errors removed — mostly Decimal-related No overload/Unsupported operand types reports, now gone because the arithmetic stubs accept SupportsFloat (matching runtime acceptance of Decimal/numpy scalars).

  • 11 added, all of which are Decimal in-place operations and one matrix overload artifact:

    (a) In-place assignment (5): dec_iadd_var += var etc. report Incompatible types in assignment (Expr vs Decimal). Root cause: mypy expects x += y to keep x's declared type (Decimal), but Decimal.__iadd__ falls back to Expr.__radd__ and the result is an Expr. At runtime the variable simply rebinds to the expression — correct behavior, statically flagged by mypy. This is a generator artifact (it probes Decimal as an in-place left operand, which Python semantics say changes the variable type).

    (b) Decimal method precedence (5): assert_type(dec_ipow_var, ExpExpr) etc. report Expression is of type "Decimal". mypy resolves Decimal ** expr via the standard library Decimal.__pow__ and keeps the inferred type Decimal, while at runtime it falls back to the PySCIPOpt reflection path and returns the expression. Static-analysis only; runtime correct.

    (c) Matrix overload overlap (1): var <= mvar1d infers ExprCons instead of MatrixExprCons. SupportsFloat is structural and MatrixVariable (an ndarray subclass) implements __float__, so it matches the scalar overload before the matrix one. See point 3.

3. # type: ignore[overload-cannot-match] on 10 stubs

The arithmetic/comparison operand type went from float (nominal — matches by inheritance, so MatrixVariable never matched) to SupportsFloat (structural — matches anything with __float__, and ndarray has one). mypy therefore considers the scalar overload to cover the matrix overloads; the 10 overload-cannot-match/overload-overlap suppressions are unavoidable — reordering overloads or using a custom protocol does not help (verified experimentally), and there is no type-system way to express "has __float__ but is not ndarray". Runtime is unaffected; mypy's inference for var + mvar1d degrades to the scalar type behind the ignore, which is a known trade-off.

Summary: the 11 new baseline entries all document known static-analysis limitations of Decimal in-place operations and the structural overlap of SupportsFloat with the ndarray subclasses; runtime behavior is correct in every case.

@Joao-Dionisio

Joao-Dionisio commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Hey @Zeroto521 , there seem to be multiple unrelated things in this PR, can you please split them up?

Declare Expr.terms, GenExpr.children, SumExpr/ProdExpr.constant,
PowExpr.expo and Constant.number as \cdef readonly\, so assigning to
them from Python raises AttributeError. The fields remain writable from
Cython, which is all the internal code needs.

No type annotations are added on purpose: typing the fields (e.g.
\cdef readonly double number\) would change runtime behaviour, as
values would be converted on assignment (a Decimal would become a
float at construction) and repr() would change accordingly.

Only the Cython declaration changes; runtime behaviour, the type stubs
and the mypy baseline are unaffected.
@Zeroto521 Zeroto521 changed the title Make expression fields read-only and remove dead VarExpr.var Make expression fields read-only Oct 15, 2026

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants