Skip to content

Add ExtendedGCD builtin - #2076

Merged
rocky merged 16 commits into
masterfrom
add-ExtendedGCD
Oct 31, 2026
Merged

rocky merged 16 commits into
masterfrom
add-ExtendedGCD

Conversation

@rocky

@rocky rocky commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Add a first cut at ExtendedGCD.
Go over the divlike docs.

Todo: extend to polynomials like PolynomialGCD, etc.

Start to revise to more modern Python conventions via ruff.

@rocky rocky changed the title Add extended gcd Add ExtendedGCD builtin Oct 29, 2026
@rocky
rocky requested review from mmatera and tombouctou October 30, 2026 00:16

@tombouctou tombouctou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review of the ExtendedGCD addition. The first two inline comments are about wrong results and a crash, which I confirmed by running the PR branch and comparing with WMA 15. The rest are smaller: docs links, type hints, and a formatting bug in Expression.__repr__/__str__.

Comment thread mathics/builtin/intfns/divlike.py Resolved
Comment thread mathics/builtin/intfns/divlike.py Outdated
https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm</url> (<url>
:SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex</url>, <url>
:WMA:
https://reference.wolfram.com/language/ref/GCD.html</url>)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This link points to GCD.html; it should be ExtendedGCD.html.

Comment thread mathics/builtin/intfns/divlike.py Outdated
https://reference.wolfram.com/language/ref/GCD.html</url>
<url>:Greatest common divisor:
https://en.wikipedia.org/wiki/Greatest_common_divisor</url> (<url>
:SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex</url>, <url>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This SymPy link is for GCD, but it points to IntegerRing.gcdex (the extended GCD) rather than gcd.

Comment thread mathics/builtin/intfns/divlike.py Outdated
<url>:Modular exponentiation:
https://en.wikipedia.org/wiki/Modular_exponentiation</url> SymPy: <url>:mod_inverse:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse</url>, <url>
https://en.wikipedia.org/wiki/Modular_exponentiation</url> (<url>:SymPy:
:mod_inverse:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse</url>, <url>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This <url> has two labels, :SymPy: and :mod_inverse:, so the renderer takes :SymPy: as the label and :mod_inverse:https://… as the target, which gives a broken link.

Comment thread mathics/eval/intfns/divlike.py Outdated
@@ -1,29 +1,66 @@
"""
b"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

b""" makes this a bytes literal, not a docstring, so the module's __doc__ is None. It looks like a stray keystroke.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually, yes, "b" is a glitch on my keyboard (which is rather old, and the printed letters have worn off). Usually, there is a string of b's, and I try to catch those.

Comment thread mathics/eval/intfns/divlike.py Outdated


def eval_GCD(ns: tuple) -> Optional[Integer]:
def eval_ExtendedGCD(arguments: tuple | Integer) -> Integer | None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The return annotation says Integer | None, but the function returns a ListExpression. Also, gcdex_list's -> (list, int) is a tuple expression rather than a type hint; tuple[list[int], int] would be.

On line 49: with a single negative argument, ExtendedGCD[-5] gives {-5, {1}}, while WMA gives {5, {-1}}. The len(numbers) == 1 branch of gcdex_list has the same problem.

if isinstance(arguments, Integer):
return ListExpression(arguments, ListExpression(Integer1))
integers: list[int] = []
for element in arguments.elements:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With a single argument that is not an Integer, arguments is the atom itself, which has no .elements. ExtendedGCD[x] and ExtendedGCD[2.5] raise AttributeError: … has no attribute 'elements' out of the evaluator. WMA leaves ExtendedGCD[x] unevaluated.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A check was added in mathics/buiiltin/divlike.py.

The reason it was not added here is that the general philosophy is that general argument checking is done in the Builtin class. That way, evaluation functions work a little faster. If there is ever compilation, these eval functions will make up bytecode instructions.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That makes sense, thanks for explaining. Keeping the eval functions free of argument checks, so they stay fast and could someday be compiled, is a good reason. ExtendedGCD[x] and ExtendedGCD[2.5] now stay unevaluated, as in WMA. One small difference: WMA also gives ExtendedGCD::exact: Argument 2.5 in ExtendedGCD[2.5] is not an exact number. for the inexact case.

Comment thread mathics/core/expression.py Outdated
def __repr__(self) -> str:
return "<Expression: %s[%s]>" % (
repr(self.head),
return f"<Expression: {self.head!r}[%s]>" % (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This mixes an f-string with %-formatting, so the text of self.head!r is itself treated as a format string. A head containing % breaks it: repr(Expression(String("50%"), Integer(1))) raises ValueError: unsupported format character, and %s in a head raises "not enough arguments". __str__ has the same problem. Interpolating the elements in the same f-string avoids it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In the next commit, I'll revert this change since I am not confident how to fix.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here is a fix that keeps everything in one f-string, so nothing from the head or the elements is read as a format spec:

    def __repr__(self) -> str:
        elements = ", ".join(repr(element) for element in self.elements)
        return f"<Expression: {self.head!r}[{elements}]>"

    def __str__(self) -> str:
        elements = ", ".join(str(element) for element in self.elements)
        return f"{self.head}[{elements}]"

The revert brought __repr__ back to master's %-formatting, which was safe because the % applied to the whole template. __str__ in the PR still has f"{self.head}[%s]" % (...), so str(Expression(String("50%"), Integer(1))) still raises ValueError. Either the f-string version above or master's "%s[%s]" % (str(self.head), ...) fixes it.

Comment thread mathics/core/expression.py Outdated
in Monomial's __cmp() for the reverse ordering when
comparing symbols in the monomial.
"""
exps: dict[tuple[str, str], float | complex] = {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This dict is overwritten by exps, coefficient = monomial before it is used. _build_element_order is on a hot path (canonical ordering), so this allocates a dict on every call for nothing. A bare annotation, exps: dict[...], would keep the type without the allocation.

@rocky

rocky commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

@tombouctou: Thanks for the very detailed review. I think I addressed everything, but I still make a lot of mistakes. (That's why I've worked so much on debuggers.)

Feel free to suggest more changes though, or suggest a fix for the change I don't feel confient in addressing.

@tombouctou

Copy link
Copy Markdown
Collaborator

@rocky Thank you for going through every comment in such detail, and for explaining the reasoning where you chose differently: checking arguments in the Builtin class and keeping the eval functions lean is a good principle, and I'll keep it in mind in my own PRs.

I re-ran the PR head (703897b) against WMA 15. ExtendedGCD[3, 2], ExtendedGCD[12, 8, 6], ExtendedGCD[-4, 6] and the non-integer arguments now all agree. Two single-argument cases still differ:

PR WMA
ExtendedGCD[-5] {-5, {-1}} {5, {-1}}
ExtendedGCD[0] {0, {-1}} {0, {0}}

The GCD should be the absolute value, with the sign moved into the coefficient, and for 0 the coefficient is 0. For example:

    if isinstance(arguments, Integer):
        n = arguments.value
        return ListExpression(Integer(abs(n)), ListExpression(Integer((n > 0) - (n < 0))))

In the __repr__ thread I left a fix for the formatting issue. __str__ in the PR still has the f-string/% mix.

@rocky
rocky merged commit ce535a6 into master Oct 31, 2026
15 checks passed
@rocky
rocky deleted the add-ExtendedGCD branch October 31, 2026 14:18
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.

2 participants