codecalc 0.1.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codecalc/exact.py ADDED
@@ -0,0 +1,969 @@
1
+ """Exact arithmetic and programmer-mode tools, ported from the Claude `calc`
2
+ skill (~/.claude/skills/calc/calc.py) into codecalc as structured (JSON)
3
+ MCP tools.
4
+
5
+ Everything numeric is exact by default (Fraction / sympy), mirrors the
6
+ original's design: print the exact value first, the decimal approximation
7
+ second, because the two disagreeing is usually the bug.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import ast
13
+ import math
14
+ import statistics
15
+ import struct
16
+ from datetime import UTC, datetime
17
+ from decimal import Decimal, getcontext
18
+ from fractions import Fraction
19
+
20
+ from .guarded import guarded_call
21
+ from .optional import require
22
+ from .safe_expr import reject_unsafe
23
+
24
+ getcontext().prec = 50
25
+
26
+ # ─────────────────────────── exact AST evaluator ───────────────────────────
27
+
28
+ # Whitelisted callables for the AST evaluator. Deliberately NOT eval(): parsing
29
+ # to an AST and walking a closed set of node types means no attribute access,
30
+ # imports, comprehensions, or lambdas are reachable — same safety design as the
31
+ # original calc.py.
32
+ _FUNCS = {k: getattr(math, k) for k in dir(math)
33
+ if not k.startswith("_") and callable(getattr(math, k))}
34
+ _FUNCS.update({"abs": abs, "min": min, "max": max, "round": round,
35
+ "int": int, "float": float})
36
+
37
+ #: Functions that are EXACT over rationals and must never see a float.
38
+ #: math.* has to go through float — that is inherent — but these do not, and
39
+ #: routing them through float silently destroyed exactness in the one module
40
+ #: whose entire purpose is exactness: abs(-1/3) returned
41
+ #: 3333333333333333/10000000000000000. Worse, the loss was data-dependent:
42
+ #: max(1/3, 1/2) came back as exactly 1/2 because 1/2 is binary-representable,
43
+ #: so the bug looked absent about half the time.
44
+ _EXACT_FUNCS = {"abs", "min", "max", "int"}
45
+ #: pi/e/tau are IRRATIONAL — no Fraction is equal to them. These are the exact
46
+ #: rational value of the binary64 approximation, which is the best a rational
47
+ #: evaluator can do, but a result computed from one is not exact and must not be
48
+ #: labelled as such. `_uses_approx_const` tracks that so eval_exact can report it.
49
+ _CONSTS = {"pi": Fraction(math.pi), "e": Fraction(math.e),
50
+ "tau": Fraction(math.tau)}
51
+
52
+ _BIN = {
53
+ ast.Add: lambda a, b: a + b,
54
+ ast.Sub: lambda a, b: a - b,
55
+ ast.Mult: lambda a, b: a * b,
56
+ ast.Div: lambda a, b: a / b,
57
+ ast.FloorDiv: lambda a, b: a // b,
58
+ ast.Mod: lambda a, b: a % b,
59
+ ast.Pow: lambda a, b: _pow_exact(a, b),
60
+ ast.BitAnd: lambda a, b: _int_op(a, b, "&"),
61
+ ast.BitOr: lambda a, b: _int_op(a, b, "|"),
62
+ ast.BitXor: lambda a, b: _int_op(a, b, "^"),
63
+ ast.LShift: lambda a, b: _int_op(a, b, "<<"),
64
+ ast.RShift: lambda a, b: _int_op(a, b, ">>"),
65
+ }
66
+ _CMP = {
67
+ ast.Gt: lambda a, b: a > b,
68
+ ast.GtE: lambda a, b: a >= b,
69
+ ast.Lt: lambda a, b: a < b,
70
+ ast.LtE: lambda a, b: a <= b,
71
+ ast.Eq: lambda a, b: a == b,
72
+ ast.NotEq: lambda a, b: a != b,
73
+ }
74
+
75
+
76
+ def _int_op(a: Fraction, b: Fraction, op: str) -> Fraction:
77
+ """Bitwise on exact rationals: legal only when both sides are integers."""
78
+ for side, v in (("left", a), ("right", b)):
79
+ if v.denominator != 1:
80
+ raise ValueError(f"bitwise {op} needs integers; {side} operand is {v}")
81
+ ai, bi = int(a), int(b)
82
+ if op in ("<<", ">>") and bi < 0:
83
+ raise ValueError(f"negative shift count {bi}")
84
+ return Fraction({"<<": ai << bi, ">>": ai >> bi,
85
+ "&": ai & bi, "|": ai | bi, "^": ai ^ bi}[op])
86
+
87
+
88
+ #: Exponent magnitude and predicted-result-size guards for `**`. Both are
89
+ #: checked BEFORE the exponentiation runs, not after: `a ** b` on Fraction has
90
+ #: no magnitude guard of its own, so `2**10**6` (the inner `10**6` evaluates
91
+ #: first, then `2**1000000`) takes seconds and `2**10**7` does not return in
92
+ #: any reasonable time, both reachable from ordinary expression syntax with no
93
+ #: big-integer literals involved.
94
+ _MAX_POW_EXPONENT = 10_000
95
+ _MAX_POW_RESULT_BITS = 50_000
96
+
97
+
98
+ def _pow_exact(a: Fraction, b: Fraction) -> Fraction:
99
+ """a ** b, bounded before the computation runs rather than after.
100
+
101
+ The exponent's own magnitude is checked first (catches `2**10**7` for
102
+ free, since the huge exponent itself is rejected before any power is
103
+ taken). For an integer exponent, the result's bit length is then
104
+ predicted from the base's bit length — O(1) work — and rejected if it
105
+ would blow the budget, which catches a huge BASE with a modest exponent
106
+ too.
107
+ """
108
+ if abs(b) > _MAX_POW_EXPONENT:
109
+ raise ValueError(
110
+ f"exponent {b} exceeds the limit ({_MAX_POW_EXPONENT}) for exact exponentiation"
111
+ )
112
+ if b.denominator == 1 and a != 0:
113
+ base_bits = max(abs(a.numerator).bit_length(), abs(a.denominator).bit_length())
114
+ predicted_bits = base_bits * abs(int(b))
115
+ if predicted_bits > _MAX_POW_RESULT_BITS:
116
+ raise ValueError(
117
+ f"{a}**{b} would need ~{predicted_bits} bits to represent, "
118
+ f"exceeding the limit ({_MAX_POW_RESULT_BITS}) for exact exponentiation"
119
+ )
120
+ return a ** b
121
+
122
+
123
+ #: Names consulted during the current eval_exact() call, reset per call.
124
+ _APPROX_CONSTS_USED: list[str] = []
125
+ #: math.* functions called during this eval. They must go through float — there
126
+ #: is no exact rational sqrt — so a NON-INTEGER result from one is an
127
+ #: approximation however exactly the Fraction prints. sqrt(144) -> 12 is still
128
+ #: the true answer; sqrt(2) -> 1414.../1000... is not.
129
+ _FLOAT_FUNCS_USED: list[str] = []
130
+
131
+
132
+ def _ev(node):
133
+ """Walk a closed set of AST nodes; anything else is refused."""
134
+ if isinstance(node, ast.Expression):
135
+ return _ev(node.body)
136
+ if isinstance(node, ast.Constant):
137
+ if isinstance(node.value, bool) or not isinstance(node.value, (int, float)):
138
+ raise TypeError(f"unsupported literal {node.value!r}")
139
+ return Fraction(str(node.value))
140
+ if isinstance(node, ast.Name):
141
+ if node.id in _CONSTS:
142
+ _APPROX_CONSTS_USED.append(node.id)
143
+ return _CONSTS[node.id]
144
+ raise ValueError(f"unknown name {node.id!r}")
145
+ if isinstance(node, ast.UnaryOp):
146
+ if isinstance(node.op, ast.UAdd):
147
+ return _ev(node.operand)
148
+ if isinstance(node.op, ast.USub):
149
+ return -_ev(node.operand)
150
+ if isinstance(node.op, ast.Invert):
151
+ return Fraction(~int(_ev(node.operand)))
152
+ raise ValueError("unsupported unary op")
153
+ if isinstance(node, ast.BinOp):
154
+ fn = _BIN.get(type(node.op))
155
+ if fn is None:
156
+ raise ValueError(f"unsupported operator {type(node.op).__name__}")
157
+ return fn(_ev(node.left), _ev(node.right))
158
+ if isinstance(node, ast.Compare):
159
+ if len(node.ops) != 1:
160
+ raise ValueError("chained comparisons not supported")
161
+ fn = _CMP.get(type(node.ops[0]))
162
+ if fn is None:
163
+ raise ValueError(f"unsupported comparison {type(node.ops[0]).__name__}")
164
+ return fn(_ev(node.left), _ev(node.comparators[0]))
165
+ if isinstance(node, ast.Call):
166
+ if not isinstance(node.func, ast.Name) or node.func.id not in _FUNCS:
167
+ raise ValueError("only whitelisted math functions are callable")
168
+ args = [_ev(a) for a in node.args]
169
+ fn = _FUNCS[node.func.id]
170
+ if node.func.id == "round" and len(args) == 2:
171
+ return Fraction(round(args[0] * 10 ** args[1]) / 10 ** args[1])
172
+ if node.func.id in _EXACT_FUNCS:
173
+ # Fraction supports these directly; keep the exact value.
174
+ return Fraction(fn(*args))
175
+ _FLOAT_FUNCS_USED.append(node.func.id)
176
+ try:
177
+ result = fn(*[float(a) for a in args])
178
+ except (TypeError, ValueError, OverflowError):
179
+ # functions needing exact ints (comb, factorial, ...): coerce
180
+ coerced = []
181
+ for a in args:
182
+ if isinstance(a, Fraction) and a.denominator == 1:
183
+ coerced.append(int(a))
184
+ else:
185
+ coerced.append(a)
186
+ result = fn(*coerced)
187
+ if isinstance(result, float):
188
+ if not math.isfinite(result):
189
+ raise ValueError(f"{node.func.id} produced non-finite result")
190
+ return Fraction(str(result))
191
+ return Fraction(result)
192
+ raise ValueError(f"unsupported syntax: {type(node).__name__}")
193
+
194
+
195
+ #: DoS guard on input size, consistent with logic.py's _MAX_EXPR_LEN.
196
+ _MAX_EXPR_LEN = 2000
197
+
198
+
199
+ def eval_exact(expr: str) -> dict:
200
+ """Evaluate an expression with EXACT rational arithmetic.
201
+
202
+ `0.1 + 0.2 == 0.3` is True here (False in plain Python). Supports
203
+ arithmetic, comparisons, bitwise ops on integers, math.* functions,
204
+ pi/e/tau. Returns the exact value plus decimal approximation.
205
+
206
+ GUARDED, and it was the last symbolic tool that was not. #78 put the six
207
+ SymPy tools in a killable child; this one has its own AST evaluator over
208
+ math.*, so it was not in that set and nobody noticed. It bounded `**`
209
+ through _MAX_POW_EXPONENT and bounded nothing about function arguments —
210
+ `factorial(10000000)` ran until the process was killed from outside.
211
+
212
+ Two layers, same as everywhere else here. reject_unsafe screens the string
213
+ (it carries the heavy-argument cap); the guard bounds whatever the screen
214
+ did not anticipate, which for this tool is every one of the ~50 callables
215
+ dir(math) exposes.
216
+ """
217
+ _warm_exact()
218
+ return guarded_call(_eval_exact, expr)
219
+
220
+
221
+ def _eval_exact(expr: str) -> dict:
222
+ """The evaluation itself. Runs inside the guard, never called directly."""
223
+ if len(expr) > _MAX_EXPR_LEN:
224
+ return {"ok": False, "error": f"expression too long (max {_MAX_EXPR_LEN} chars)"}
225
+ # The screen the other four functions in this file already call, and this
226
+ # one never did. It carries the heavy-argument cap, which is what stops
227
+ # factorial(<huge>) before the evaluator reaches math.factorial.
228
+ _bad = reject_unsafe(expr)
229
+ if _bad:
230
+ return {"ok": False, "error": _bad}
231
+ try:
232
+ tree = ast.parse(expr, mode="eval")
233
+ except SyntaxError as exc:
234
+ return {"ok": False, "error": f"parse error: {exc.msg}"}
235
+ _APPROX_CONSTS_USED.clear()
236
+ _FLOAT_FUNCS_USED.clear()
237
+ try:
238
+ result = _ev(tree)
239
+ except Exception as exc:
240
+ return {"ok": False, "error": str(exc)}
241
+ approx_consts = sorted(set(_APPROX_CONSTS_USED))
242
+ float_funcs = sorted(set(_FLOAT_FUNCS_USED))
243
+
244
+ if isinstance(result, bool):
245
+ return {"ok": True, "value": result, "type": "bool"}
246
+ if isinstance(result, Fraction):
247
+ # The pow guard above bounds the CPU cost of exponentiation, but its
248
+ # budget is looser than Python's own int->str conversion ceiling
249
+ # (4300 digits by default), so a result can still be too big to
250
+ # FORMAT even though it was cheap to compute. Catch that here rather
251
+ # than let it escape as a bare ValueError with no tool-level context.
252
+ try:
253
+ d = Decimal(result.numerator) / Decimal(result.denominator)
254
+ approx = f"{d:.12f}".rstrip("0").rstrip(".")
255
+ exact = str(result) if result.denominator != 1 else str(result.numerator)
256
+ except ValueError as exc:
257
+ return {"ok": False, "error": f"result too large to format: {exc}"}
258
+ out = {"ok": True, "value": exact, "approx": approx,
259
+ # `exact` previously meant "the exact form differs from the
260
+ # 12-dp decimal", which is a different claim from the one the
261
+ # name makes. It now means what it says: is this value the true
262
+ # answer? An expression touching pi/e/tau is not, because those
263
+ # are irrational and were substituted with the rational value of
264
+ # their binary64 approximation.
265
+ # A float-path function that produced a non-integer gives a
266
+ # rational that merely PRINTS exactly. Integer results (sqrt(144))
267
+ # are still the true answer, so they stay exact.
268
+ "exact": not approx_consts and not (float_funcs and result.denominator != 1),
269
+ "differs_from_decimal": exact != approx,
270
+ "type": "rational"}
271
+ if float_funcs and result.denominator != 1:
272
+ out.setdefault("approximated", []).extend(float_funcs)
273
+ out["note"] = (f"{', '.join(float_funcs)} has no exact rational form; "
274
+ "the value shown is the exact rational of a binary64 "
275
+ "approximation, so this result is NOT exact")
276
+ if approx_consts:
277
+ out.setdefault("approximated", []).extend(approx_consts)
278
+ out["note"] = (f"{', '.join(approx_consts)} is irrational; the value "
279
+ "used is the exact rational form of its binary64 "
280
+ "approximation, so this result is NOT exact")
281
+ return out
282
+ return {"ok": True, "value": str(result), "type": type(result).__name__}
283
+
284
+
285
+ # ────────────────────────────── threshold cmp ──────────────────────────────
286
+
287
+ _CMP_SYMS = {">": _CMP[ast.Gt], ">=": _CMP[ast.GtE], "<": _CMP[ast.Lt],
288
+ "<=": _CMP[ast.LtE], "==": _CMP[ast.Eq], "=": _CMP[ast.Eq],
289
+ "!=": _CMP[ast.NotEq]}
290
+
291
+
292
+ def compare_threshold(a: str, op: str, b: str) -> dict:
293
+ """Exact threshold comparison: does `a OP b` hold?
294
+
295
+ Prints both sides as exact fractions plus the verdict and the shortfall,
296
+ so a threshold comparison written out cannot be gotten backwards.
297
+ `op` is one of ==, !=, >, >=, <, <= (= accepted for ==).
298
+ """
299
+ op = op.strip()
300
+ if op not in _CMP_SYMS:
301
+ return {"ok": False, "error": f"bad operator {op!r}; use ==, !=, >, >=, <, <="}
302
+ ra = eval_exact(a)
303
+ rb = eval_exact(b)
304
+ if not (ra.get("ok") and rb.get("ok")):
305
+ return {"ok": False, "error": f"left: {ra.get('error')}; right: {rb.get('error')}"}
306
+ try:
307
+ fa = Fraction(ra["value"] if ra["type"] == "rational" else str(ra["value"]))
308
+ fb = Fraction(rb["value"] if rb["type"] == "rational" else str(rb["value"]))
309
+ except (ValueError, ZeroDivisionError):
310
+ return {"ok": False, "error": "operands must be numeric"}
311
+ holds = _CMP_SYMS[op](fa, fb)
312
+ diff = fa - fb
313
+ return {"ok": True, "a": str(fa), "b": str(fb), "op": op,
314
+ "holds": bool(holds), "difference": str(diff),
315
+ "shortfall": None if holds else str(-diff)}
316
+
317
+
318
+ def percentage(part: str, total: str) -> dict:
319
+ """Exact share and percentage of PART / TOTAL."""
320
+ try:
321
+ p, t = Fraction(part), Fraction(total)
322
+ except (ValueError, ZeroDivisionError):
323
+ return {"ok": False, "error": "part and total must be numbers"}
324
+ if t == 0:
325
+ return {"ok": False, "error": "total is zero"}
326
+ share = p / t
327
+ d = Decimal(share.numerator) / Decimal(share.denominator)
328
+ return {"ok": True, "part": str(p), "total": str(t),
329
+ "share": f"{share.numerator}/{share.denominator}",
330
+ "percent": round(float(d) * 100, 6)}
331
+
332
+
333
+ # ─────────────────────────────── stats / pctl ──────────────────────────────
334
+
335
+ def _first_non_finite(nums: list[float]) -> int | None:
336
+ """Index of the first nan/inf in NUMS, or None if every value is finite.
337
+
338
+ nan/inf reach these tools as ordinary floats and used to raise out of
339
+ them uncaught — Fraction()/int() conversions elsewhere in this module
340
+ assume a finite value and blow up on the ones that are not
341
+ (AttributeError from statistics.stdev on a NaN-poisoned sum, ValueError
342
+ converting NaN to int, ...). Naming the offending index turns that into
343
+ an answerable question about the caller's input.
344
+ """
345
+ for i, n in enumerate(nums):
346
+ if not math.isfinite(float(n)):
347
+ return i
348
+ return None
349
+
350
+
351
+ def stats(nums: list[float]) -> dict:
352
+ """mean, median, stdev (sample), and coefficient of variation (CV)."""
353
+ if len(nums) < 2:
354
+ return {"ok": False, "error": "need at least 2 numbers"}
355
+ idx = _first_non_finite(nums)
356
+ if idx is not None:
357
+ return {"ok": False,
358
+ "error": f"nums[{idx}] = {nums[idx]!r} is not finite; stats needs finite numbers"}
359
+ vals = [float(n) for n in nums]
360
+ mean = statistics.fmean(vals)
361
+ stdev = statistics.stdev(vals) if len(vals) > 1 else 0.0
362
+ # CV is undefined when the mean is 0. It used to be float("nan"), which is
363
+ # (a) not valid JSON — json.dumps emits a bare NaN that strict parsers
364
+ # reject — and (b) silently classified as fine, because `nan > 0.2` is
365
+ # False, so the note read "within noise budget" for a number that does not
366
+ # exist. None serialises as null and says what it means.
367
+ cv = stdev / mean if mean else None
368
+ if cv is None:
369
+ note = "CV undefined: the mean is 0, so relative variation has no meaning"
370
+ elif cv > 0.2:
371
+ note = "run-to-run noise swamps the effect (CV > 0.2)"
372
+ else:
373
+ note = "within noise budget"
374
+ return {"ok": True, "n": len(vals), "mean": mean,
375
+ "median": statistics.median(vals),
376
+ "stdev": stdev, "cv": cv, "cv_note": note}
377
+
378
+
379
+ def percentiles(nums: list[float]) -> dict:
380
+ """p50/p90/p95/p99 by nearest-rank AND linear interpolation."""
381
+ if not nums:
382
+ return {"ok": False, "error": "need at least 1 number"}
383
+ idx = _first_non_finite(nums)
384
+ if idx is not None:
385
+ return {"ok": False,
386
+ "error": f"nums[{idx}] = {nums[idx]!r} is not finite; percentiles needs finite numbers"}
387
+ vals = sorted(float(n) for n in nums)
388
+ n = len(vals)
389
+ out = {}
390
+ for p in (50, 90, 95, 99):
391
+ # nearest-rank: ceil(p/100 * n), 1-indexed
392
+ rank = max(1, math.ceil(p / 100 * n))
393
+ nr = vals[rank - 1]
394
+ # linear interpolation (numpy-style)
395
+ idx = p / 100 * (n - 1)
396
+ lo, hi = math.floor(idx), math.ceil(idx)
397
+ interp = vals[lo] + (vals[hi] - vals[lo]) * (idx - lo)
398
+ out[f"p{p}"] = {"nearest_rank": nr, "interpolated": round(interp, 6)}
399
+ warn = "n < 100: p99 is just the maximum wearing a label" if n < 100 else None
400
+ return {"ok": True, "n": n, "percentiles": out, "warning": warn}
401
+
402
+
403
+ def collision_prob(items: int, bits: int) -> dict:
404
+ """Birthday-bound hash collision probability: 1 - exp(-n^2 / (2*2^b))."""
405
+ n, b = int(items), int(bits)
406
+ if b <= 0 or n < 0:
407
+ return {"ok": False, "error": "items >= 0 and bits >= 1"}
408
+ if n < 2:
409
+ return {"ok": True, "probability": 0.0, "items": n, "bits": b}
410
+ p = 1.0 - math.exp(-(n * n) / (2.0 * (2 ** b)))
411
+ return {"ok": True, "items": n, "bits": b, "probability": p,
412
+ "percent": round(p * 100, 6)}
413
+
414
+
415
+ # ────────────────────────────── bytes / dur / time ─────────────────────────
416
+
417
+ def data_sizes(n: int) -> dict:
418
+ """Byte sizes both ways: binary (KiB/MiB) and decimal (KB/MB)."""
419
+ n = int(n)
420
+ return {"ok": True, "bytes": n,
421
+ "binary": {"KiB": n / 1024, "MiB": n / 1024 ** 2,
422
+ "GiB": n / 1024 ** 3, "TiB": n / 1024 ** 4},
423
+ "decimal": {"KB": n / 1000, "MB": n / 1000 ** 2,
424
+ "GB": n / 1000 ** 3, "TB": n / 1000 ** 4}}
425
+
426
+
427
+ def human_duration(seconds: float) -> dict:
428
+ """Humanised duration plus per-day and per-30d rates."""
429
+ s = float(seconds)
430
+ if not math.isfinite(s):
431
+ return {"ok": False, "error": f"seconds = {s!r} is not finite"}
432
+ if s < 0:
433
+ return {"ok": False, "error": "negative duration"}
434
+ units = [("d", 86400), ("h", 3600), ("m", 60), ("s", 1)]
435
+ parts = []
436
+ for name, size in units:
437
+ if s >= size or name == "s":
438
+ q = int(s // size)
439
+ parts.append(f"{q}{name}")
440
+ s -= q * size
441
+ return {"ok": True, "seconds": float(seconds),
442
+ "human": " ".join(parts),
443
+ "per_day": round(float(seconds) / 86400, 6) if seconds else 0.0,
444
+ "per_30d": round(float(seconds) / 2592000, 6) if seconds else 0.0}
445
+
446
+
447
+ def epoch_time(n: str) -> dict:
448
+ """Epoch seconds/millis/micros/nanos to ISO 8601 UTC."""
449
+ try:
450
+ v = int(n)
451
+ except ValueError:
452
+ return {"ok": False, "error": "epoch must be an integer"}
453
+ if v < 0:
454
+ return {"ok": False, "error": "negative epoch"}
455
+ # 10**17 was below a real nanosecond timestamp (now ~1.8e18), so the
456
+ # advertised ns support was unreachable: every ns value was rejected as
457
+ # "implausibly large". The true ceiling is the largest value that is
458
+ # plausible in the COARSEST unit still accepted — nanoseconds — which is
459
+ # ~3e20 for the year 11000. Anything beyond that is not a timestamp.
460
+ if v > 10 ** 21:
461
+ return {"ok": False, "error": "implausibly large epoch"}
462
+ results = {}
463
+ for name, unit in (("seconds", 10 ** 0), ("millis", 10 ** 3),
464
+ ("micros", 10 ** 6), ("nanos", 10 ** 9)):
465
+ secs = v / unit
466
+ # Lower bound raised off zero: interpreting a microsecond timestamp as
467
+ # nanoseconds put it in 1970, and that 1970 reading was reported
468
+ # alongside the real one. "Implausible readings suppressed" has to mean
469
+ # both ends. 10**8 ~ 1973-03; anything earlier is a unit mismatch, not a
470
+ # date someone meant.
471
+ if 10 ** 8 <= secs <= 10 ** 10: # plausible range ~1973..2286
472
+ try:
473
+ results[name] = datetime.fromtimestamp(secs, tz=UTC).isoformat()
474
+ except (OverflowError, OSError, ValueError):
475
+ pass
476
+ if not results:
477
+ return {"ok": False, "error": "no plausible unit found"}
478
+ return {"ok": True, "input": v, "interpretations": results}
479
+
480
+
481
+ # ────────────────────────────── base / radix ───────────────────────────────
482
+
483
+ def base_repr(n: int, width: int | None = None) -> dict:
484
+ """hex/oct/bin, two's complement at WIDTH, and signed-overflow detection."""
485
+ n = int(n)
486
+ w = width if width is not None else None
487
+ out = {"ok": True, "value": n,
488
+ "hex": f"{n:x}", "oct": f"{n:o}", "bin": f"{n:b}"}
489
+ if w is not None:
490
+ w = int(w)
491
+ if w <= 0:
492
+ return {"ok": False, "error": "width must be positive"}
493
+ lo, hi = -(2 ** (w - 1)), 2 ** (w - 1) - 1
494
+ uhi = 2 ** w - 1
495
+ if not (lo <= n <= uhi):
496
+ out["overflow"] = (f"does not fit in signed i{w} "
497
+ f"(range {lo}..{hi}) nor unsigned u{w}")
498
+ elif lo <= n <= hi:
499
+ out["signed_i" + str(w)] = n
500
+ out["unsigned_u" + str(w)] = n if n >= 0 else n + 2 ** w
501
+ out["two_complement_bin"] = f"{n & (2 ** w - 1):0{w}b}"
502
+ else: # only fits unsigned
503
+ out["signed_i" + str(w)] = n - 2 ** w
504
+ out["unsigned_u" + str(w)] = n
505
+ out["two_complement_bin"] = f"{n:0{w}b}"
506
+ out["wrapped_signed"] = n - 2 ** w
507
+ out["signed_overflow"] = (f"does not fit in signed i{w} "
508
+ f"(range {lo}..{hi}); fits u{w} only")
509
+ return out
510
+
511
+
512
+ _RADIX_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"
513
+
514
+
515
+ def radix_convert(value: str, from_base: int = 10, to_base: int = 10) -> dict:
516
+ """Convert a value between any bases 2..36, fractions included.
517
+
518
+ Non-terminating results (e.g. 0.1 in base 2) are flagged.
519
+ """
520
+ fb, tb = int(from_base), int(to_base)
521
+ if not (2 <= fb <= 36 and 2 <= tb <= 36):
522
+ return {"ok": False, "error": "bases must be 2..36"}
523
+ s = value.strip().lower()
524
+ neg = s.startswith("-")
525
+ if neg:
526
+ s = s[1:]
527
+
528
+ def _parse_digit(c):
529
+ v = _RADIX_DIGITS.index(c) if c in _RADIX_DIGITS else -1
530
+ if v < 0 or v >= fb:
531
+ raise ValueError(f"invalid digit {c!r} for base {fb}")
532
+ return v
533
+
534
+ if "." in s:
535
+ ip, fp = s.split(".", 1)
536
+ else:
537
+ ip, fp = s, ""
538
+ try:
539
+ whole = 0
540
+ for c in ip:
541
+ whole = whole * fb + _parse_digit(c)
542
+ frac_num, frac_den = 0, 1
543
+ for c in fp:
544
+ frac_num = frac_num * fb + _parse_digit(c)
545
+ frac_den *= fb
546
+ num = whole * frac_den + frac_num
547
+ den = frac_den
548
+ except ValueError as exc:
549
+ return {"ok": False, "error": str(exc)}
550
+ if den == 0:
551
+ return {"ok": True, "value": "0", "non_terminating": False}
552
+
553
+ # integer part to target base
554
+ whole = num // den
555
+ rem = num % den
556
+ int_digits = []
557
+ if whole == 0:
558
+ int_digits = ["0"]
559
+ while whole:
560
+ int_digits.append(_RADIX_DIGITS[whole % tb])
561
+ whole //= tb
562
+ int_str = "".join(reversed(int_digits))
563
+
564
+ # fractional part; terminates iff denominator (reduced) has only prime
565
+ # factors that also divide tb
566
+ from math import gcd
567
+ g = gcd(rem, den)
568
+ rn, rd = rem // g, den // g
569
+ # Reduce a SEPARATE copy of rd's prime factors to those in tb to decide
570
+ # `terminates`. This must not mutate rd itself: rd (paired with rn) is the
571
+ # denominator digits get generated from below, and rn/rd == rem/den (the
572
+ # true value) only while rd stays the reduced denominator. Stripping rd
573
+ # down to 1 in-place — the previous bug — left digit generation dividing
574
+ # the reduced numerator rn by the UNREDUCED den, i.e. computing rn/den,
575
+ # which is rem/den divided by g an extra time and is not the input at all.
576
+ check = rd
577
+ t = tb
578
+ while check > 1:
579
+ d = math.gcd(check, t)
580
+ if d == 1:
581
+ break
582
+ check //= d
583
+ terminates = check == 1
584
+
585
+ frac_digits = []
586
+ r = rn
587
+ seen = {}
588
+ while r and not terminates and len(frac_digits) < 64:
589
+ if r in seen:
590
+ break
591
+ seen[r] = len(frac_digits)
592
+ r *= tb
593
+ frac_digits.append(_RADIX_DIGITS[r // rd])
594
+ r %= rd
595
+ if terminates:
596
+ guard = 0
597
+ while r and guard < 100:
598
+ r *= tb
599
+ frac_digits.append(_RADIX_DIGITS[r // rd])
600
+ r %= rd
601
+ guard += 1
602
+ frac_str = "".join(frac_digits) if frac_digits else "0"
603
+ else:
604
+ # finite-length truncation marked with ellipsis
605
+ frac_str = "".join(frac_digits) + "…"
606
+
607
+ sign = "-" if neg else ""
608
+ if frac_digits:
609
+ return {"ok": True, "value": f"{sign}{int_str}.{frac_str}",
610
+ "non_terminating": not terminates}
611
+ return {"ok": True, "value": f"{sign}{int_str}", "non_terminating": False}
612
+
613
+ # ────────────────────────────── float / int / bits ─────────────────────────
614
+
615
+ def float_repr(x: float) -> dict:
616
+ """What binary64 actually stores for x: exact value, bits, ULP, neighbours."""
617
+ x = float(x)
618
+ if math.isnan(x) or math.isinf(x):
619
+ return {"ok": True, "literal": repr(x), "stored": repr(x),
620
+ "bits": "nan" if math.isnan(x) else ("inf" if x > 0 else "-inf"),
621
+ "exact": True, "note": "non-finite value"}
622
+ bits = struct.unpack(">Q", struct.pack(">d", x))[0]
623
+ sign = bits >> 63
624
+ exp = (bits >> 52) & 0x7FF
625
+ frac = bits & 0xFFFFFFFFFFFFF
626
+ # exact rational value
627
+ if exp == 0: # subnormal
628
+ value = Fraction(frac, 2 ** 52) * Fraction(1, 2 ** 1022)
629
+ else:
630
+ mant = Fraction(2 ** 52 + frac, 2 ** 52)
631
+ e = exp - 1023
632
+ value = mant * (Fraction(2) ** e if e >= 0 else Fraction(1, 2 ** (-e)))
633
+ if sign:
634
+ value = -value
635
+ # Neighbours and ULP: math.nextafter, not the raw bit pattern. The bit
636
+ # pattern for -0.0 is 0x8000000000000000; decrementing that (the old
637
+ # "move away from zero" step for a negative sign) underflows to
638
+ # 0x7fffffffffffffff, which is a NaN encoding, not a neighbour. -0.0 is
639
+ # also the likeliest negative-zero input to a float-inspection tool.
640
+ # math.nextafter has no such special case: it is defined in terms of the
641
+ # real number line, so ±0.0 (which compare equal) both correctly step to
642
+ # ±5e-324. The bit-level path stays for bits_hex/exact/stored, which are
643
+ # legitimately about the stored encoding rather than the value's
644
+ # neighbours.
645
+ #
646
+ # This deliberately CHANGES +0.0 too, whose `prev` was -0.0 and is now
647
+ # -5e-324. The old special case reasoned that the adjacent bit pattern
648
+ # below +0.0 is -0.0, which is true of the ENCODING and false of the
649
+ # VALUE: -0.0 == 0.0, so it is not a neighbour on the number line. Mixing
650
+ # those two questions in one field is what produced the -0.0 bug. The
651
+ # encoding question is still answered, by bits_hex/sign/stored.
652
+ next_val = math.nextafter(x, math.inf)
653
+ prev_val = math.nextafter(x, -math.inf)
654
+ ulp = abs(next_val - x)
655
+ # is the literal representable? (compare exact rational of literal vs stored)
656
+ exact = (Decimal(str(x)) == Decimal(value.numerator) / Decimal(value.denominator))
657
+ d = Decimal(value.numerator) / Decimal(value.denominator)
658
+ stored = f"{d:.40f}".rstrip("0").rstrip(".")
659
+ err = value - Fraction(str(x))
660
+ rel = abs(float(err) / float(value)) if value else 0.0
661
+ return {"ok": True, "literal": repr(x), "stored": stored,
662
+ "bits_hex": f"0x{bits:016x}", "sign": sign, "exponent": exp,
663
+ "fraction_hex": f"0x{frac:013x}",
664
+ "exact": bool(exact),
665
+ "error": str(err), "relative_error": rel,
666
+ "ulp": ulp, "prev": prev_val, "next": next_val,
667
+ "note": (">2^53: consecutive integers no longer distinguishable"
668
+ if abs(x) > 2 ** 53 else None)}
669
+
670
+
671
+ def int_widths(n: int) -> dict:
672
+ """Which widths hold n, and the wrapped value where they do not."""
673
+ n = int(n)
674
+ out = {"ok": True, "value": n}
675
+ fits = []
676
+ wraps = []
677
+ for w in (8, 16, 32, 64):
678
+ lo, hi = -(2 ** (w - 1)), 2 ** (w - 1) - 1
679
+ uhi = 2 ** w - 1
680
+ if lo <= n <= hi:
681
+ fits.append(f"i{w}")
682
+ if n >= 0:
683
+ fits.append(f"u{w}")
684
+ elif 0 <= n <= uhi:
685
+ # Fits unsigned only. The `n >= 0` guard is the fix: the test used
686
+ # to be `n <= uhi`, which is true for EVERY negative number, so
687
+ # int_widths(-200) claimed u8 and int_widths(-2**70) claimed
688
+ # u8/u16/u32/u64. A negative value fits no unsigned width at all.
689
+ fits.append(f"u{w}")
690
+ wraps.append(f"i{w}: wraps to {n - 2 ** w}")
691
+ elif n < lo:
692
+ wraps.append(f"i{w}/u{w}: too negative (below {lo})")
693
+ else:
694
+ wraps.append(f"i{w}/u{w}: too large (above {uhi})")
695
+ out["fits"] = fits
696
+ out["wraps"] = wraps
697
+ out["note"] = (">2^53 cannot round-trip through a JS number or JSON float"
698
+ if abs(n) > 2 ** 53 else None)
699
+ return out
700
+
701
+
702
+ def bit_analysis(n: int, align: int | None = None) -> dict:
703
+ """popcount, trailing zeros, next pow2, alignment padding."""
704
+ n = int(n)
705
+ out = {"ok": True, "value": n,
706
+ "popcount": n.bit_count(),
707
+ "bit_length": n.bit_length() if n >= 0 else None,
708
+ "trailing_zeros": (n & -n).bit_length() - 1 if n else None,
709
+ "is_power_of_two": n > 0 and (n & (n - 1)) == 0}
710
+ if n > 0:
711
+ out["next_power_of_two"] = 1 << (n - 1).bit_length()
712
+ if align is not None:
713
+ a = int(align)
714
+ if a <= 0:
715
+ return {"ok": False, "error": "alignment must be positive"}
716
+ pad = (-n) % a
717
+ out["aligned_to"] = a
718
+ out["padding_needed"] = pad
719
+ out["aligned_value"] = n + pad
720
+ return out
721
+
722
+
723
+ # ────────────────────────────── bitop (programmer mode) ────────────────────
724
+
725
+ _BITOP_OPS = ("and", "or", "xor", "nand", "nor", "xnor", "not",
726
+ "shl", "shr", "sar", "rol", "ror")
727
+
728
+
729
+ def bitop(a: int, op: str, b: int | None = None, width: int = 64) -> dict:
730
+ """Programmer-mode bit operations at a fixed width.
731
+
732
+ Every result prints unsigned, signed, hex, octal and nibble-grouped
733
+ binary at the declared width (8/16/32/64). shr = logical (zero-fill),
734
+ sar = arithmetic (sign-propagating). A left shift that drops bits is
735
+ flagged OVERFLOW with the unbounded answer shown.
736
+ """
737
+ op = op.lower()
738
+ w = int(width)
739
+ if w not in (8, 16, 32, 64):
740
+ return {"ok": False, "error": "width must be 8/16/32/64"}
741
+ mask = (1 << w) - 1
742
+ sign_bit = 1 << (w - 1)
743
+ a = int(a) & mask
744
+
745
+ def to_signed(v):
746
+ return v - (1 << w) if v & sign_bit else v
747
+
748
+ def show(v):
749
+ u = v & mask
750
+ return {"unsigned": u, "signed": to_signed(u),
751
+ "hex": f"0x{u:0{w // 4}x}",
752
+ "oct": f"0o{u:0{(w + 2) // 3}o}",
753
+ "bin": f"{u:0{w}b}"}
754
+
755
+ if op == "not":
756
+ r = ~a & mask
757
+ return {"ok": True, "op": op, "width": w, **show(r)}
758
+ if b is None:
759
+ return {"ok": False, "error": f"{op} needs two operands"}
760
+ # For shl/shr/sar/rol/ror, `b` is a SHIFT COUNT, not a value at this
761
+ # width — masking it with the width mask truncated the count itself:
762
+ # bitop(1, "shl", 256, 8) masked 256 to 0 and shifted by 0, silently
763
+ # turning "shift far past the width" into "don't shift at all". Keep the
764
+ # count as a real (unmasked) integer and reject a negative one outright;
765
+ # only and/or/xor/nand/nor/xnor treat `b` as a VALUE, which does get
766
+ # masked to the declared width like `a` above.
767
+ if op in ("shl", "shr", "sar", "rol", "ror"):
768
+ b = int(b)
769
+ if b < 0:
770
+ return {"ok": False, "error": f"negative shift/rotate count {b}"}
771
+ else:
772
+ b = int(b) & mask
773
+ if op == "and":
774
+ return {"ok": True, "op": op, "width": w, **show(a & b)}
775
+ if op == "or":
776
+ return {"ok": True, "op": op, "width": w, **show(a | b)}
777
+ if op == "xor":
778
+ return {"ok": True, "op": op, "width": w, **show(a ^ b)}
779
+ if op == "nand":
780
+ return {"ok": True, "op": op, "width": w, **show(~(a & b))}
781
+ if op == "nor":
782
+ return {"ok": True, "op": op, "width": w, **show(~(a | b))}
783
+ if op == "xnor":
784
+ return {"ok": True, "op": op, "width": w, **show(~(a ^ b))}
785
+ if op == "shl": # logical left
786
+ if b >= w:
787
+ return {"ok": True, "op": op, "width": w, **show(0),
788
+ "overflow": f"shift {b} >= width {w}"}
789
+ r = a << b
790
+ if r > mask:
791
+ return {"ok": True, "op": op, "width": w, **show(r),
792
+ "overflow": True, "unbounded": r}
793
+ return {"ok": True, "op": op, "width": w, **show(r)}
794
+ if op == "shr": # logical right: zero-fill
795
+ if b >= w:
796
+ return {"ok": True, "op": op, "width": w, "unsigned": 0,
797
+ "signed": 0, "hex": "0x0", "oct": "0o0",
798
+ "bin": "0" * w}
799
+ return {"ok": True, "op": op, "width": w, **show(a >> b)}
800
+ if op == "sar": # arithmetic right: sign-propagating
801
+ sa = to_signed(a)
802
+ r = sa >> b if b < w else (-1 if sa < 0 else 0)
803
+ return {"ok": True, "op": op, "width": w, **show(r & mask)}
804
+ if op == "rol":
805
+ b %= w
806
+ r = ((a << b) | (a >> (w - b))) & mask if b else a
807
+ return {"ok": True, "op": op, "width": w, **show(r)}
808
+ if op == "ror":
809
+ b %= w
810
+ r = ((a >> b) | (a << (w - b))) & mask if b else a
811
+ return {"ok": True, "op": op, "width": w, **show(r)}
812
+ return {"ok": False, "error": f"unknown op {op!r}; use {', '.join(_BITOP_OPS)}"}
813
+
814
+ # ────────────────────────────── symbolic (sympy) ───────────────────────────
815
+
816
+ def _sympy():
817
+ sp = require("sympy")
818
+ return sp
819
+
820
+
821
+ def _algebraic_equiv(a: str, b: str) -> dict:
822
+ """Are two expressions algebraically identical? (refactor verification)
823
+
824
+ Identity over symbolic reals says nothing about float rounding, integer
825
+ truncation or modular overflow — usually where the real difference lives.
826
+ """
827
+ # SymPy evaluates what it parses (see codecalc/safe_expr.py). Screen the
828
+ # caller's string BEFORE it reaches sympify, not after.
829
+ for _name, _val in (("a", a), ("b", b)):
830
+ _bad = reject_unsafe(_val)
831
+ if _bad:
832
+ return {"ok": False, "error": f"{_name}: {_bad}"}
833
+ sp = _sympy()
834
+ try:
835
+ ea = sp.sympify(a)
836
+ eb = sp.sympify(b)
837
+ except Exception as exc:
838
+ return {"ok": False, "error": f"parse: {exc}"}
839
+ diff = sp.simplify(ea - eb)
840
+ identical = diff == 0
841
+ return {"ok": True, "a": str(ea), "b": str(eb),
842
+ "identical": bool(identical), "difference": str(diff),
843
+ "caveat": ("identity over symbolic reals says nothing about float "
844
+ "rounding, integer truncation or modular overflow") if not identical else None}
845
+
846
+
847
+ def _solve_expression(expr: str, var: str = "x") -> dict:
848
+ """Solve for a root or crossover: `x**2 - 4 = 0`, `2*x + 1 = 7`."""
849
+ # SymPy evaluates what it parses (see codecalc/safe_expr.py). Screen the
850
+ # caller's string BEFORE it reaches sympify, not after.
851
+ # Screened per SIDE, because this splits on '=' before sympify and the
852
+ # screen refuses '=' — the parts are what reach the parser, so the parts
853
+ # are what must be checked.
854
+ for _part in (expr.split("=", 1) if ("=" in expr and "==" not in expr) else [expr]):
855
+ _bad = reject_unsafe(_part)
856
+ if _bad:
857
+ return {"ok": False, "error": _bad}
858
+ sp = _sympy()
859
+ try:
860
+ if "=" in expr and "==" not in expr:
861
+ lhs, rhs = expr.split("=", 1)
862
+ eq = sp.Eq(sp.sympify(lhs), sp.sympify(rhs))
863
+ else:
864
+ eq = sp.Eq(sp.sympify(expr), 0)
865
+ sym = sp.Symbol(var.strip())
866
+ solutions = sp.solve(eq, sym)
867
+ except Exception as exc:
868
+ return {"ok": False, "error": str(exc)}
869
+ return {"ok": True, "equation": str(eq), "variable": var,
870
+ "solutions": [str(s) for s in solutions]}
871
+
872
+
873
+ def _limit_expression(expr: str, var: str = "x", point: str = "oo") -> dict:
874
+ """Asymptotic behaviour: limit of EXPR as var -> point (default oo)."""
875
+ # SymPy evaluates what it parses (see codecalc/safe_expr.py). Screen the
876
+ # caller's string BEFORE it reaches sympify, not after.
877
+ for _name, _val in (("expr", expr), ("point", point)):
878
+ _bad = reject_unsafe(_val)
879
+ if _bad:
880
+ return {"ok": False, "error": f"{_name}: {_bad}"}
881
+ sp = _sympy()
882
+ try:
883
+ e = sp.sympify(expr)
884
+ sym = sp.Symbol(var.strip())
885
+ pt = sp.oo if point.strip() == "oo" else sp.sympify(point.strip())
886
+ result = sp.limit(e, sym, pt)
887
+ except Exception as exc:
888
+ return {"ok": False, "error": str(exc)}
889
+ return {"ok": True, "expression": str(e), "variable": var,
890
+ "point": str(pt), "limit": str(result)}
891
+
892
+
893
+ def _simplify_expression(expr: str) -> dict:
894
+ """Simplified, factored and expanded forms of an expression."""
895
+ # SymPy evaluates what it parses (see codecalc/safe_expr.py). Screen the
896
+ # caller's string BEFORE it reaches sympify, not after.
897
+ _bad = reject_unsafe(expr)
898
+ if _bad:
899
+ return {"ok": False, "error": _bad}
900
+ sp = _sympy()
901
+ try:
902
+ e = sp.sympify(expr)
903
+ return {"ok": True, "original": str(e),
904
+ "simplified": str(sp.simplify(e)),
905
+ "factored": str(sp.factor(e)),
906
+ "expanded": str(sp.expand(e))}
907
+ except Exception as exc:
908
+ return {"ok": False, "error": str(exc)}
909
+
910
+
911
+ # ── the bound (#84) ────────────────────────────────────────────────────────
912
+ # Each tool above reaches SymPy's parser, which evaluates what it parses. The
913
+ # screen in safe_expr runs first and still does the work it always did; these
914
+ # wrappers add the part a denylist cannot provide, which is a ceiling that did
915
+ # not have to anticipate the expression.
916
+ #
917
+ # The bodies are untouched — renamed to _name and called through guarded_call,
918
+ # so the result shapes a caller already depends on are unchanged except for an
919
+ # `unenforced` entry where the platform has no fork.
920
+
921
+ _EXACT_WARMED = False
922
+
923
+
924
+ def _warm_exact() -> None:
925
+ """Warm the parent for the paths THESE tools take, once.
926
+
927
+ Not shared with logic.py's warm set, and not merged into it: measured,
928
+ solve/limit/simplify reach different SymPy submodules than
929
+ evaluate_expression does, and warming the wrong ones costs the same startup
930
+ while leaving the fork to re-import the right ones on every call. #78
931
+ measured that mistake at +196ms on a single expression.
932
+ """
933
+ global _EXACT_WARMED
934
+ if _EXACT_WARMED:
935
+ return
936
+ _EXACT_WARMED = True
937
+ try:
938
+ _simplify_expression("x**2 + 2*x + 1")
939
+ _solve_expression("x**2 - 4 = 0", "x")
940
+ _limit_expression("1/x", "x", "oo")
941
+ _algebraic_equiv("(x+1)**2", "x**2 + 2*x + 1")
942
+ except Exception:
943
+ # Warming is an optimisation; a failure here must not take down the
944
+ # call the caller actually made.
945
+ pass
946
+
947
+
948
+ def algebraic_equiv(a: str, b: str) -> dict:
949
+ """Are two expressions algebraically identical? (refactor verification)"""
950
+ _warm_exact()
951
+ return guarded_call(_algebraic_equiv, a, b)
952
+
953
+
954
+ def solve_expression(expr: str, var: str = "x") -> dict:
955
+ """Solve for a root or crossover: `x**2 - 4 = 0`, `2*x + 1 = 7`."""
956
+ _warm_exact()
957
+ return guarded_call(_solve_expression, expr, var)
958
+
959
+
960
+ def limit_expression(expr: str, var: str = "x", point: str = "oo") -> dict:
961
+ """Asymptotic behaviour: limit of EXPR as var -> point (default oo)."""
962
+ _warm_exact()
963
+ return guarded_call(_limit_expression, expr, var, point)
964
+
965
+
966
+ def simplify_expression(expr: str) -> dict:
967
+ """Simplified, factored and expanded forms of an expression."""
968
+ _warm_exact()
969
+ return guarded_call(_simplify_expression, expr)