Repository navigation
Add ExtendedGCD builtin - #2076
Conversation
36debd9 to
57bfb82
Compare
Corrected typos in docstrings and comments for clarity.
…to add-ExtendedGCD
tombouctou
left a comment
There was a problem hiding this comment.
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__.
| 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>) |
There was a problem hiding this comment.
This link points to GCD.html; it should be ExtendedGCD.html.
| 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> |
There was a problem hiding this comment.
This SymPy link is for GCD, but it points to IntegerRing.gcdex (the extended GCD) rather than gcd.
| <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> |
There was a problem hiding this comment.
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.
| @@ -1,29 +1,66 @@ | |||
| """ | |||
| b""" | |||
There was a problem hiding this comment.
b""" makes this a bytes literal, not a docstring, so the module's __doc__ is None. It looks like a stray keystroke.
There was a problem hiding this comment.
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.
|
|
||
|
|
||
| def eval_GCD(ns: tuple) -> Optional[Integer]: | ||
| def eval_ExtendedGCD(arguments: tuple | Integer) -> Integer | None: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| def __repr__(self) -> str: | ||
| return "<Expression: %s[%s]>" % ( | ||
| repr(self.head), | ||
| return f"<Expression: {self.head!r}[%s]>" % ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
In the next commit, I'll revert this change since I am not confident how to fix.
There was a problem hiding this comment.
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.
| in Monomial's __cmp() for the reverse ordering when | ||
| comparing symbols in the monomial. | ||
| """ | ||
| exps: dict[tuple[str, str], float | complex] = {} |
There was a problem hiding this comment.
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.
Co-authored-by: tombouctou <vissi@vissi.su>
|
@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. |
|
@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.
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 |
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.