Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 47c-import-values-length #654 +/- ##
==========================================================
Coverage 98.65% 98.65%
==========================================================
Files 105 105
Lines 29510 29678 +168
Branches 3452 3472 +20
==========================================================
+ Hits 29113 29280 +167
+ Misses 261 259 -2
- Partials 136 139 +3 🚀 New features to boost your workflow:
|
eriknw
force-pushed
the
52-scalar-infix-literal-operand
branch
from
October 28, 2026 13:21
ea6fb88 to
2ff3579
Compare
Behavior fix. Since 2023.1.0, `x & s` and `x | s`, with a literal x and
a Scalar s, built the expression with s on the left and x on the right,
so the op applied to it ran with its operands swapped, and every op that
does not commute gave wrong values, silently:
s = Scalar.from_value(5)
binary.minus(1 & s) # was 4, now -4
binary.truediv(1 | s) # was 5.0, now 0.2
binary.first(1 & s) # was 5, now 1
binary.lt(1 & s) # was False, now True
So did rminus, cdiv, rdiv, pow, the other comparisons and
is-comparisons, and the rest of the ops that do not commute. So did the
defaults of an ewise_union, each of which went with the other operand:
with an empty Scalar e, `binary.minus(1 | e, left_default=0,
right_default=10)` was -1 and is now -9. And so did chains:
`binary.minus(1 & (2 & s))` was 2 and is now 4.
The expression now keeps the literal where it was written, as it is, and
the op applied to it types it: Scalar's ewise_mult, ewise_add and
ewise_union take a literal on the left as they take one on the right,
typed beside the Scalar by literals._literal_operand. A Python number is
weak, as beside `-`: `binary.minus(2 & int8_s)` is INT8 and
`binary.minus(0.5 & int8_s)` is FP64, as `2 - int8_s` and `0.5 - int8_s`
are. Beside a UDT, a lifted op such as minus types the literal as numpy
would and `first` makes it an element of the UDT.
How a literal is typed depends on the op (300 beside INT8 is an
OverflowError for `-` and is compared as it is by `<`), so the literal
is not converted when `&` or `|` builds the expression. Converting it
then, with its own dtype, would get the typing wrong: the 2 of
`int8_s & 2` would be INT64 under every op, where `int8_s - 2` keeps it
INT8, and `0.5 & int8_udt_s` would cut 0.5 to 0 in the UDT.
`op(x & y)` now asks the expression to call its left operand's method
(InfixExprBase._call_left), so a Scalar expression can pass a literal
there.
Two more errors with a literal operand are fixed:
- Computing a BOOL expression with a literal by itself, as
`(s & True).new()` and `(False | s).new()` do, raised AttributeError:
'bool' object has no attribute 'dtype'. It now types the literal for
LAND or LOR and computes, or raises the usual TypeError for a dtype
other than BOOL.
- `s & object()` gave an expression that failed only when an op was
applied; it now raises TypeError at once, as `s + object()` does. The
check types the literal as `==` would and drops it. Beside an infix
expression, as in `(s & t) & object()`, it raises when the op is
applied.
Behavior fix. Since 2023.1.0, a Scalar's reflected operators (`lit - s`,
`lit / s`, `lit ** s` and so on, with a literal on the left) computed
the op's commutes_to with the Scalar first, rminus(s, lit). That fails
where no such op exists:
1 - udt_s, 1 / udt_s, 1 // udt_s, (1, 2) - udt_s
# KeyError: 'rminus does not work with ...'; no reflected op
# takes a UDT, though `udt_s - 1` and `1 - udt_vector` work
7 % s # TypeError for every dtype: numpy.mod has no reflected op
2 ** s # TypeError without Numba: rpow is a Numba function
Now the literal becomes a Scalar on the left, typed beside s as in
`binary.minus(1 & s)`, and the op itself is applied: `1 - udt_s` is
[-4, -6] for udt_s = [5, 7], and `7 % int8_s` is 2 for 5.
`lit ** s` now gives what `binary.pow(lit, s)` and `lit ** vector` give.
That changes four kinds of result, where rpow, a Numba function,
differed:
- an integer power out of range saturates, as GraphBLAS's pow does,
where rpow wrapped: `3 ** int8_s` for 5 was -13 and is now 127, and
`0 ** int8_s` for -3 was 0 and is now 127;
- `False ** bool_s` is BOOL, not INT8;
- `0 ** complex_s` is 0, not nan (for 1+2j), and other complex powers
may differ in the last bit;
- a Python int beyond INT64 beside a BOOL Scalar, as in
`(2**64 - 1) ** bool_s`, raises OverflowError, as
`bool_s ** (2**64 - 1)` and `(2**64 - 1) ** bool_vector` do, where it
gave a UINT64.
Nothing else changes: for `+ - * / // % ** ^`, nine Scalars (INT8,
INT64, UINT8, FP32, FP64, BOOL, empty) and eight literals, every result
the previous code gave is the same but those of `**`; and for the seven
that are not empty, `lit OP s` now matches `binary.op(lit, s)` for each
of `+ - * / // % **` (it did not in 66 of 392 cases).
Behavior fix. Since 2023.1.0, showing a Scalar `&` or `|` expression
raised in two cases:
repr(s & 1), repr(1 | s), repr(t & True), and their _repr_html_
# AttributeError: 'int' object has no attribute 'name'
repr(s & s) # s not BOOL, with autocompute on (the default)
# TypeError: Bad dtypes for `x & y`! Automatic computation ...
A literal operand now shows its value, as an unnamed Scalar does in an
expression, and its own type where a dtype goes, since it has no dtype
until an op types it:
gb.ScalarEwiseMultExpr left_dtype right_dtype
s_1 & 1 INT8 int
The text repr now computes the expression to show its result only where
the HTML repr, and the reprs of Vector and Matrix infix expressions,
do: for `x @ y`, and for `x & y` and `x | y` of BOOL operands. A literal
counts as BOOL when it is a bool, so `t & True` shows its result.
A chain such as `(s & t) & u` still raises in a repr, as it does for
Vector and Matrix.
… infix expression A behavior fix. When an operand of `&`, `|` or `@` was itself an infix expression and the sizes did not match, building the error crashed in _mismatched, which called _dummy(obj) while _dummy took a second parameter, obj_type, that it never used (since #517). Before: >>> (v & v) & w # v of size 3, w of size 4 TypeError: _dummy() missing 1 required positional argument: 'obj_type' After: >>> (v & v) & w DimensionMismatch: ... The same held with the expression on the right (w & (v & v)), for `|`, for Matrix operands ((A & A) & B), and for `@` ((A @ A) @ w). _dummy now takes only obj. Tests cover Vector and Matrix with the expression on either side, for `|`, `&` and `@`.
eriknw
force-pushed
the
52-scalar-infix-literal-operand
branch
from
October 29, 2026 02:01
2ff3579 to
ce63433
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #643. Four behavior fixes, each with before and after in its commit message. All
are on main: the first three since "Computation between scalars" (#358, first released in
2023.1.0), the fourth since #517.
A literal keeps its side of a Scalar's
&and|.x & sandx | s, with a literalx and a Scalar s, built the expression with s on the left, so the op applied to it ran with
its operands swapped. Every op that does not commute gave wrong values, silently:
The literal now stays where it was written, as it is, and the op applied to the expression
types it beside the Scalar with
literals._literal_operand, aslit - sandop(lit, s)type it: a Python number is weak, so
binary.minus(2 & int8_s)is INT8 andbinary.minus(0.5 & int8_s)is FP64. It is not converted when&builds the expression,because its type depends on the op: 300 beside INT8 is an OverflowError for minus and is
compared as it is by lt, and converting
int8_s & 2with the literal's own dtype wouldmake it INT64 under every op, or cut 0.5 to 0 in an integer UDT.
op(x & y)now asks theexpression to call its left operand's method (
InfixExprBase._call_left), so a Scalarexpression can pass a literal there.
Two more errors with a literal operand:
(s & True).new()and(False | s).new()raisedAttributeError: 'bool' object has no attribute 'dtype'and now compute (or raise theusual TypeError for a dtype other than BOOL), and
s & object()raises TypeError at once,as
s + object()does, instead of giving an expression that failed only when an op wasapplied.
lit - sand the like apply minus(lit, s), not rminus(s, lit). The reflectedoperators computed the op's
commutes_towith the Scalar first, which fails where no suchop exists:
Now
1 - udt_sis [-4, -6] for udt_s = [5, 7], and7 % int8_sis 2 for 5.lit ** snow gives what
binary.pow(lit, s)andlit ** vectorgive, which changes four kinds ofresult where rpow differed: an integer power out of range saturates (
3 ** int8_sfor 5was -13, now 127);
False ** bool_sis BOOL, not INT8;0 ** complex_sis 0, not nan(other complex powers may differ in the last bit); and
(2**64 - 1) ** bool_sraisesOverflowError, as
bool_s ** (2**64 - 1)does. Nothing else changes: for+ - * / // % ** ^, nine Scalars and eight literals, every result but those of**is thesame, and
lit OP snow equalsbinary.op(lit, s)in all 392 cases with a value (itdiffered in 66).
The repr of a Scalar
&or|expression.repr(s & 1),repr(1 | s),repr(t & True)and their_repr_html_raisedAttributeError: 'int' object has no attribute 'name', andrepr(s & s)for a non-BOOL s raised "Bad dtypes forx & y!" underautocompute. A literal operand now shows its value, and its own type where a dtype goes,
since it has no dtype until an op types it:
The text repr computes the result only where the HTML repr and the Vector and Matrix infix
reprs do: for
x @ y, and for&and|of BOOL operands. A chain such as(s & t) & ustill raises in a repr, as it does for Vector and Matrix.
DimensionMismatch, not TypeError, for a size mismatch beside an infix expression.
Building the error crashed when an operand was itself an infix expression:
The same held with the expression on the right, for
|and@, and for Matrix operands._dummynever used its second parameter, and its only caller passes one argument. Thisone-line fix is independent of the rest; it was found while testing main's coverage gaps
(#656).
Tests:
test_scalar_infix_literal_keeps_its_sideand its UDT twin and additions totest_bad_ewiseandtest_bad_matmulin test_infix.py;test_scalar_operators_literal_on_the_leftand its UDT twin in test_scalar.py;test_scalar_infix_expr_repr_literalandtest_scalar_infix_expr_repr_autointest_formatting.py. Each fails on the commit before its fix.
Checked locally: every op with a literal on either side of
&and|, andewise_unionwith defaults, against
op(lit, s)and against the explicit Scalar ewise call: 47 ops(every non-commutative one, and the commutative ones), 6 Scalar dtypes and 11 literals,
21822 cases, 6151 of them wrong before (in 30 ops) and none after; a UDT battery (record
and array UDTs, lifted ops, first, second and a user op; tuple, list, dict and number
literals) went from 156 wrong to none. The full suite passes with
--runslow, withoutNumba on both backends, and in CI's C-scalar pass; on Python 3.15, numpy 2.2 and
SuiteSparse:GraphBLAS 8.2 the only failures come from the local environments
(
test_version, and pandas 3.1 in the Python 3.15 one, see #591).Not changed here: a numpy scalar on the left (
np.int16(2) - s) is still computed by numpythrough
Scalar.__array__. #605, higher in the stack, un-gates the two new formatting testswhen pandas is absent.