Arithmetic

In ASPAlchemy, you write math expressions using standard Python notation, with Python’s usual precedence, and should almost never need to think about what’s going on in clingo. The renderer translates your Python expressions into clingo, adding parentheses where needed so that what Python thinks is going on is exactly how clingo sees it too.

There was an “almost” in there. Clingo works only with 32-bit integers, and its integer arithmetic differs from Python’s in a small number of edge cases. ASPAlchemy can’t change how clingo evaluates an expression, so those are the places where you do need to know the difference.

This page covers how the operators map between the two languages, the rules of integer math in clingo, the edges of the 32-bit range, and what happens when you divide by zero.

Operators and precedence

Python ASP rendering Notes
x + y x + y  
x - y x - y  
x * y x * y  
x // y x / y integer division; Python’s / is refused
x % y x \ y modulo
x ** y x ** y right-associative in ASP
x & y x & y bitwise and
x \| y x ? y bitwise or
x ^ y x ^ y bitwise xor
-x -x refused at the int32 floor
Compl(x) ~x bitwise complement; Python’s ~ is reserved for default negation
abs(x) \|x\| refused at the int32 floor

Bitwise complement is spelled Compl(x) because ~ already means default negation on literals. Write ~X on a variable and ASPAlchemy tells you so:

from aspalchemy import Number, Variable

X, Y = Variable("X"), Variable("Y")
>>> ~X
Traceback (most recent call last):
    ...
TypeError: ~ is default negation and applies to literals ...

ASPAlchemy uses Python’s precedence rules, and renders parentheses to ensure that gringo interprets expressions as intended. In case you’re curious though, gringo’s precedence goes: ^, then ?, then &, then + -, then * / \, then ** (right-associative), then unary - and complement. Two traps for Python intuitions: the bitwise operators bind looser than +, and unary minus binds tighter than ** (in gringo -2**2 is (-2)**2 = 4; in Python it is -(2**2) = -4).

Generally speaking, ASPAlchemy emits as few parentheses as possible. Power and the bitwise operators always get explicit parentheses, so nobody has to remember gringo’s table to read the output.

a, b, c = Number(1), Number(2), Number(3)
>>> (a + b * c).render()  # classic arithmetic: minimal parentheses
'1 + 2 * 3'
>>> (a - (b - c)).render()  # have to keep these parentheses
'1 - (2 - 3)'
>>> (a + (b - c)).render()  # ... but a `+` parent has nothing to protect
'1 + 2 - 3'
>>> (a + b**c).render()  # power and bitwise: explicit parentheses, always
'1 + (2 ** 3)'
>>> (a**b**c).render()
'1 ** (2 ** 3)'
>>> ((a & b) | c).render()
'(1 & 2) ? 3'
>>> (-(b**b)).render()  # Python parses -2**2 as -(2**2); gringo is told this explicitly
'-(2 ** 2)'

ASPAlchemy also tidies negative numbers so the rendering reads the way you would write it by hand: X + -1 is spelled X - 1, and a subtracted negative turns into an addition. Both spellings are legal ASP, so this only changes how the term reads, never what it means.

>>> (X + Number(-1)).render()
'X - 1'
>>> (X - (-Y)).render()
'X + Y'

Integer math

Clingo has integers and nothing else: no floats, and no arithmetic on strings. Python’s / is the first division most people reach for, but it produces a float, so ASPAlchemy refuses it and points you at //:

>>> X / 2
Traceback (most recent call last):
    ...
TypeError: clingo has no true division; use // ...

Integer division throws away the remainder, which means that regrouping a product can change its value. So ASPAlchemy is cautious here: the right operand of * keeps its parentheses, because its own left term may hide a division that regrouping would not preserve. 5 * (7 / 3 * 2) is 20, while the same characters regrouped left to right, 5 * 7 / 3 * 2, are 22.

>>> (Number(5) * ((Number(7) // Number(3)) * Number(2))).render()  # Number(...) stops Python doing the math
'5 * (7 / 3 * 2)'
>>> (Number(5) * Number(3) * Number(2)).render()  # left-nested: already in order
'5 * 3 * 2'

Strings have no arithmetic in clingo at all, for any operator, so a string operand is refused when you write it rather than turning into an undefined operation at grounding:

>>> from aspalchemy import String
>>> String("a") + 1
Traceback (most recent call last):
    ...
TypeError: Strings have no arithmetic in clingo ...

Within the integers, there are two places where clingo and Python give different answers.

Integer division and modulo round differently on negatives. Python floors: -7 // 2 == -4 and -7 % 2 == 1. Clingo truncates toward zero: -7/2 == -3 and -7\2 == -1. On non-negative operands they agree.

Negative exponents. Python’s 2 ** -1 is 0.5, a float. Clingo evaluates 2**(-1) to 0, and likewise for every base but one: 0**(-1) is undefined, and behaves like a division by zero.

These come from how clingo evaluates expressions, and ASPAlchemy cannot compensate for them. To see it happen, here is clingo computing -7 // 2, next to Python doing the same:

>>> from aspalchemy import ASPProgram, Predicate
>>> Result = Predicate.define("result", ["value"])
>>> program = ASPProgram()
>>> program.fact(Result(value=Number(-7) // Number(2)))
>>> [atom["value"].value for atom in next(iter(program.solve())).atoms(Result)]
[-3]
>>> -7 // 2  # what Python would have said for the same operands
-4

Some good news though: the bitwise operators agree with Python exactly, negatives included. Both use the same infinite-precision two’s-complement definitions (-5 & 3 == 3, ~(-6) == 5).

Working with int32

Clingo integers are 32 bits, and they wrap silently. 100000 * 100000 evaluates to 1410065408 (that is 10^10 mod 2^32) and 2**40 evaluates to 0, with no warning of any kind. Python integers never overflow, so if your arithmetic can go past ±2^31, clingo will compute confidently wrong answers.

The floor of the range, −2147483648, is the sharpest edge. It is the one value with no positive counterpart, so every operation that negates is a silent no-op on it. Here are four of them, each returning the floor unchanged where Python returns 2147483648:

  clingo Python
-(-2147483648) −2147483648 2147483648
\|-2147483648\| −2147483648 2147483648
-2147483648 / -1 −2147483648 2147483648
-2147483648 * -1 −2147483648 2147483648

ASPAlchemy refuses the first two when you write them against the literal, rather than emitting a term that quietly does nothing:

>>> -Number(-2147483648)
Traceback (most recent call last):
    ...
ValueError: -(-2147483648) is a no-op in clingo: ...
>>> abs(Number(-2147483648))
Traceback (most recent call last):
    ...
ValueError: abs(-2147483648) is a no-op in clingo: ...

The other two can’t be caught that way, because X / -1 is only a problem once X turns out to be the floor, and nothing knows that until grounding. The floor itself is a perfectly good value to hold (clingo hands it back from any overflow), and adding to it or taking Compl of it are both correct. Terms that merely contain it are left as written:

>>> (X + Number(-2147483648)).render()
'X + (-2147483648)'

A word of caution: because of the floor, |x| is not always non-negative. ASPAlchemy refuses abs() of the literal, but it cannot know when a variable holds the floor, so abs(X) in a constraint is not a guaranteed magnitude. If your values can reach the floor, bound them first.

Division by zero

Raw clingo quietly drops any ground instance of a rule whose arithmetic is undefined, such as a division or modulo by zero, or 0**(-1). All it emits is an info-level message, and your program silently shrinks. ASPAlchemy’s default stop_on_log_level=INFO turns that message into a GroundingError, so a division by zero fails loudly instead. The error points at the line of Python that wrote the expression; see Clingo’s messages for what it looks like, and for how to relax the threshold if you really do want clingo’s behavior.