lumbda.py: fix two Python-tier defects (quotient precision + reader inf coercion)

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.
This commit is contained in:
russell@unturf.com 2026-06-04 12:46:29 -04:00
parent a95277cef4
commit 98d0e35a4e
No known key found for this signature in database

View file

@ -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]))