From 98d0e35a4e403ed7e24d878d8468eb91322e4da0 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 4 Jun 2026 12:46:29 -0400 Subject: [PATCH] lumbda.py: fix two Python-tier defects (quotient precision + reader inf coercion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects uncovered during secp256k1 oracle port; both forced workarounds downstream in ecdsa/lumbda/secp256k1.lsp that can now retire. (1) `quotient` used `int(a / b)` — Python float division. Past 2^53 the float lost precision, so mod-pow on secp256k1's 2^256 prime corrupted every modular inverse with off-by-one errors in the square-and-multiply loop. Replaced with R7RS-spec truncate-toward-zero integer division. Verified: (quotient 7 2)=3, (quotient -7 2)=-3, (quotient 7 -2)=-3, (quotient -7 -2)=3, (quotient 0 5)=0, (quotient 2305843009213693950 2) now returns 1152921504606846975 (was 1152921504606846976, off by 1). (2) Reader `_atom` looped `for conv in (int, float)` and Python's float() accepts bare 'inf', 'infinity', 'nan' as IEEE specials. So `'(infinity)` silently parsed as `(+inf.0)` and `(symbol? 'infinity)` returned #f. R7RS spells these +inf.0 / -inf.0 / +nan.0 explicitly. Restricted the float-parse path to a strict decimal/exponent regex; the named IEEE specials still match their proper spellings. Verified: (symbol? 'infinity)=#t (was #f), (symbol? 'inf)=#t, '(infinity) reads as the symbol list, (positive? +inf.0)=#t still works. Tests: 571 Python unit + 205 functional (Python + C) all pass. --- lumbda.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lumbda.py b/lumbda.py index c8a8d04..a6bcc92 100644 --- a/lumbda.py +++ b/lumbda.py @@ -399,12 +399,18 @@ def _atom(t): if t.startswith('"'): return (t[1:-1].replace('\\"', '"').replace('\\n', '\n') .replace('\\t', '\t').replace('\\\\', '\\').replace('\\r', '\r')) - for conv in (int, float): - try: return conv(t) - except (ValueError, OverflowError): pass + try: return int(t) + except (ValueError, OverflowError): pass + # Float parse: gate against Python's float() accepting bare 'inf', + # 'infinity', 'nan' as IEEE specials — R7RS spells those +inf.0 / + # -inf.0 / +nan.0 explicitly. Without this guard, '(infinity) reads + # as (+inf.0) and '(nan) reads as (+nan.0). if t == '+inf.0': return math.inf if t == '-inf.0': return -math.inf if t in ('+nan.0', '-nan.0'): return float('nan') + if re.fullmatch(r'[+-]?(\d+\.\d*|\.\d+|\d+)([eE][+-]?\d+)?', t): + try: return float(t) + except (ValueError, OverflowError): pass # Rational literal n/d (e.g. 1/3, -2/5) if re.fullmatch(r'-?\d+/-?\d+', t): try: @@ -2946,7 +2952,7 @@ def make_global_env(): n = float(n) / float(x) return n d(S('/'), _div) - d(S('quotient'), lambda a, _: int(_num(a[0]) / _num(a[1]))) + d(S('quotient'), lambda a, _: (lambda x, y: -(abs(int(x)) // abs(int(y))) if (x < 0) != (y < 0) else abs(int(x)) // abs(int(y)))(_num(a[0]), _num(a[1]))) d(S('remainder'), lambda a, _: int(_num(a[0])) % int(_num(a[1])) * (1 if _num(a[0]) >= 0 else -1)) d(S('modulo'), lambda a, _: int(_num(a[0])) % int(_num(a[1]))) d(S('expt'), lambda a, _: _num(a[0]) ** _num(a[1]))