Skip to content

Commit dc682be

Browse files
authored
[error] Improve display of error messages (colours, auto-inferred file name and line number) (#195)
This PR refactors Decimo’s error handling to produce Python-style tracebacks with ANSI coloring, and updates many call sites to raise typed `DecimoError[...]` variants (e.g., `ValueError`, `OverflowError`) with consistent metadata (message/function/previous error). **Changes:** - Reworked `DecimoError` formatting to mimic Python tracebacks, including chained errors and ANSI colors. - Updated many modules to raise structured `DecimoError` variants instead of raw string `Error(...)` messages. - Removed explicit `file=` parameters at many raise sites in favor of auto-captured file/line via `call_location()`.
1 parent 6b9d43c commit dc682be

24 files changed

+1010
-447
lines changed

src/decimo/bigdecimal/arithmetics.mojo

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Implements functions for mathematical operations on BigDecimal objects.
2020

2121
from std import math
2222

23+
from decimo.errors import ZeroDivisionError
2324
from decimo.rounding_mode import RoundingMode
2425

2526
# ===----------------------------------------------------------------------=== #
@@ -319,7 +320,12 @@ def true_divide(
319320
"""
320321
# Check for division by zero
321322
if y.coefficient.is_zero():
322-
raise Error("bigdecimal.arithmetics.true_divide(): Division by zero")
323+
raise Error(
324+
ZeroDivisionError(
325+
message="Division by zero.",
326+
function="true_divide()",
327+
)
328+
)
323329

324330
# Handle dividend of zero
325331
if x.coefficient.is_zero():
@@ -692,7 +698,12 @@ def true_divide_inexact(
692698

693699
# Check for division by zero
694700
if x2.coefficient.is_zero():
695-
raise Error("Division by zero")
701+
raise Error(
702+
ZeroDivisionError(
703+
message="Division by zero.",
704+
function="true_divide_inexact()",
705+
)
706+
)
696707

697708
# Handle dividend of zero
698709
if x1.coefficient.is_zero():
@@ -905,7 +916,12 @@ def truncate_divide(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal:
905916
"""
906917
# Check for division by zero
907918
if x2.coefficient.is_zero():
908-
raise Error("Division by zero")
919+
raise Error(
920+
ZeroDivisionError(
921+
message="Division by zero.",
922+
function="truncate_divide()",
923+
)
924+
)
909925

910926
# Handle dividend of zero
911927
if x1.coefficient.is_zero():
@@ -945,7 +961,12 @@ def truncate_modulo(
945961
"""
946962
# Check for division by zero
947963
if x2.coefficient.is_zero():
948-
raise Error("Division by zero")
964+
raise Error(
965+
ZeroDivisionError(
966+
message="Division by zero.",
967+
function="truncate_modulo()",
968+
)
969+
)
949970

950971
return subtract(
951972
x1,

src/decimo/bigdecimal/bigdecimal.mojo

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ from std.memory import UnsafePointer
2727
from std.python import PythonObject
2828
from std import testing
2929

30-
from decimo.errors import DecimoError
30+
from decimo.errors import DecimoError, ConversionError, ValueError
3131
from decimo.rounding_mode import RoundingMode
3232
from decimo.bigdecimal.rounding import round_to_precision
3333
from decimo.bigint10.bigint10 import BigInt10
@@ -362,14 +362,22 @@ struct BigDecimal(
362362
return Self(coefficient=BigUInt.zero(), scale=0, sign=False)
363363

364364
if value != value: # Check for NaN
365-
raise Error("`from_scalar()`: Cannot convert NaN to BigUInt")
365+
raise Error(
366+
ValueError(
367+
message="Cannot convert NaN to BigDecimal.",
368+
function="BigDecimal.from_scalar()",
369+
)
370+
)
366371
# Convert to string with full precision
367372
try:
368373
return Self.from_string(String(value))
369374
except e:
370375
raise Error(
371-
"`from_scalar()`: Cannot get decimal from string\nTrace back: "
372-
+ String(e),
376+
ConversionError(
377+
message="Cannot convert scalar to BigDecimal.",
378+
function="BigDecimal.from_scalar()",
379+
previous_error=e^,
380+
)
373381
)
374382

375383
@staticmethod
@@ -537,11 +545,9 @@ struct BigDecimal(
537545

538546
except e:
539547
raise Error(
540-
DecimoError(
541-
file="src/decimo/bigdecimal/bigdecimal.mojo",
542-
function="from_python_decimal()",
543-
message="Failed to convert Python Decimal to BigDecimal: "
544-
+ "as_tuple() returned invalid data or conversion failed.",
548+
ConversionError(
549+
message="Failed to convert Python Decimal to BigDecimal.",
550+
function="BigDecimal.from_python_decimal()",
545551
previous_error=e^,
546552
),
547553
)

src/decimo/bigdecimal/constants.mojo

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"""
1919

2020
from decimo.bigdecimal.bigdecimal import BigDecimal
21+
from decimo.errors import ValueError
2122
from decimo.bigint10.bigint10 import BigInt10
2223
from decimo.rounding_mode import RoundingMode
2324

@@ -164,7 +165,11 @@ def pi(precision: Int) raises -> BigDecimal:
164165
"""
165166

166167
if precision < 0:
167-
raise Error("Precision must be non-negative")
168+
raise Error(
169+
ValueError(
170+
message="Precision must be non-negative", function="pi()"
171+
)
172+
)
168173

169174
# TODO: When global variables are supported,
170175
# we can check if we have a cached value for the requested precision.

src/decimo/bigdecimal/exponential.mojo

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from std import math
2020

2121
from decimo.bigdecimal.bigdecimal import BigDecimal
22+
from decimo.errors import ValueError, OverflowError, ZeroDivisionError
2223
from decimo.rounding_mode import RoundingMode
2324

2425
# ===----------------------------------------------------------------------=== #
@@ -235,11 +236,20 @@ def power(
235236
# Special cases
236237
if base.coefficient.is_zero():
237238
if exponent.coefficient.is_zero():
238-
raise Error("Error in power: 0^0 is undefined")
239+
raise Error(
240+
ValueError(
241+
message="0^0 is undefined.",
242+
function="power()",
243+
)
244+
)
239245
elif exponent.sign:
240246
raise Error(
241-
"Error in power: Division by zero (negative exponent with zero"
242-
" base)"
247+
ZeroDivisionError(
248+
message=(
249+
"Division by zero (negative exponent with zero base)."
250+
),
251+
function="power()",
252+
)
243253
)
244254
else:
245255
return BigDecimal(BigUInt.zero(), 0, False)
@@ -264,8 +274,13 @@ def power(
264274
# Check for negative base with non-integer exponent
265275
if base.sign and not exponent.is_integer():
266276
raise Error(
267-
"Error in power: Negative base with non-integer exponent would"
268-
" produce a complex result"
277+
ValueError(
278+
message=(
279+
"Negative base with non-integer exponent would produce"
280+
" a complex result."
281+
),
282+
function="power()",
283+
)
269284
)
270285

271286
# Optimization for integer exponents
@@ -434,7 +449,12 @@ def root(x: BigDecimal, n: BigDecimal, precision: Int) raises -> BigDecimal:
434449

435450
# Check for n = 0
436451
if n.coefficient.is_zero():
437-
raise Error("Error in `root`: Cannot compute zeroth root")
452+
raise Error(
453+
ValueError(
454+
message="Cannot compute zeroth root.",
455+
function="root()",
456+
)
457+
)
438458

439459
# Special case for integer roots - use more efficient implementation
440460
if not n.sign:
@@ -493,8 +513,13 @@ def root(x: BigDecimal, n: BigDecimal, precision: Int) raises -> BigDecimal:
493513
var n_is_odd_reciprocal = is_odd_reciprocal(n)
494514
if not n_is_integer and not n_is_odd_reciprocal:
495515
raise Error(
496-
"Error in `root`: Cannot compute non-odd-integer root of a"
497-
" negative number"
516+
ValueError(
517+
message=(
518+
"Cannot compute non-odd-integer root of a negative"
519+
" number."
520+
),
521+
function="root()",
522+
)
498523
)
499524
elif n_is_integer:
500525
var result = integer_root(x, n, precision)
@@ -549,13 +574,28 @@ def integer_root(
549574

550575
# Handle special case: n must be a positive integer
551576
if n.sign:
552-
raise Error("Error in `root`: Root value must be positive")
577+
raise Error(
578+
ValueError(
579+
message="Root value must be positive.",
580+
function="integer_root()",
581+
)
582+
)
553583

554584
if not n.is_integer():
555-
raise Error("Error in `root`: Root value must be an integer")
585+
raise Error(
586+
ValueError(
587+
message="Root value must be an integer.",
588+
function="integer_root()",
589+
)
590+
)
556591

557592
if n.coefficient.is_zero():
558-
raise Error("Error in `root`: Cannot compute zeroth root")
593+
raise Error(
594+
ValueError(
595+
message="Cannot compute zeroth root.",
596+
function="integer_root()",
597+
)
598+
)
559599

560600
# Special case: n = 1 (1st root is just the number itself)
561601
if n.is_one():
@@ -594,7 +634,10 @@ def integer_root(
594634
result_sign = True
595635
else: # n_uint.words[0] % 2 == 0: # Even root
596636
raise Error(
597-
"Error in `root`: Cannot compute even root of a negative number"
637+
ValueError(
638+
message="Cannot compute even root of a negative number.",
639+
function="integer_root()",
640+
)
598641
)
599642

600643
# Extract n as Int for Newton's method
@@ -1183,7 +1226,10 @@ def sqrt_exact(x: BigDecimal, precision: Int) raises -> BigDecimal:
11831226
# Handle special cases
11841227
if x.sign:
11851228
raise Error(
1186-
"Error in `sqrt`: Cannot compute square root of negative number"
1229+
ValueError(
1230+
message="Cannot compute square root of a negative number.",
1231+
function="sqrt_exact()",
1232+
)
11871233
)
11881234

11891235
if x.coefficient.is_zero():
@@ -1316,7 +1362,10 @@ def sqrt_reciprocal(x: BigDecimal, precision: Int) raises -> BigDecimal:
13161362
# Handle special cases
13171363
if x.sign:
13181364
raise Error(
1319-
"Error in `sqrt`: Cannot compute square root of negative number"
1365+
ValueError(
1366+
message="Cannot compute square root of a negative number.",
1367+
function="sqrt_reciprocal()",
1368+
)
13201369
)
13211370

13221371
if x.coefficient.is_zero():
@@ -1498,7 +1547,10 @@ def sqrt_newton(x: BigDecimal, precision: Int) raises -> BigDecimal:
14981547
# Handle special cases
14991548
if x.sign:
15001549
raise Error(
1501-
"Error in `sqrt`: Cannot compute square root of negative number"
1550+
ValueError(
1551+
message="Cannot compute square root of a negative number.",
1552+
function="sqrt_newton()",
1553+
)
15021554
)
15031555

15041556
if x.coefficient.is_zero():
@@ -1583,7 +1635,10 @@ def sqrt_decimal_approach(x: BigDecimal, precision: Int) raises -> BigDecimal:
15831635
# Handle special cases
15841636
if x.sign:
15851637
raise Error(
1586-
"Error in `sqrt`: Cannot compute square root of negative number"
1638+
ValueError(
1639+
message="Cannot compute square root of a negative number.",
1640+
function="sqrt_decimal_approach()",
1641+
)
15871642
)
15881643

15891644
if x.coefficient.is_zero():
@@ -1765,7 +1820,11 @@ def exp(x: BigDecimal, precision: Int) raises -> BigDecimal:
17651820
# For very large positive values, result will overflow BigDecimal capacity
17661821
# TODO: Use BigInt10 as scale can avoid overflow in this case
17671822
if not x.sign and x.adjusted() >= 20: # x > 10^20
1768-
raise Error("Error in `exp`: Result too large to represent")
1823+
raise Error(
1824+
OverflowError(
1825+
message="Result too large to represent", function="exp()"
1826+
)
1827+
)
17691828

17701829
# For very large negative values, result will be effectively zero
17711830
if x.sign and x.adjusted() >= 20: # x < -10^20
@@ -1973,10 +2032,17 @@ def ln(
19732032
# Handle special cases
19742033
if x.sign:
19752034
raise Error(
1976-
"Error in `ln`: Cannot compute logarithm of negative number"
2035+
ValueError(
2036+
message="Cannot compute logarithm of negative number",
2037+
function="ln()",
2038+
)
19772039
)
19782040
if x.coefficient.is_zero():
1979-
raise Error("Error in `ln`: Cannot compute logarithm of zero")
2041+
raise Error(
2042+
ValueError(
2043+
message="Cannot compute logarithm of zero", function="ln()"
2044+
)
2045+
)
19802046
if x == BigDecimal(BigUInt.one(), 0, False):
19812047
return BigDecimal(BigUInt.zero(), 0, False) # ln(1) = 0
19822048

@@ -2066,21 +2132,36 @@ def log(x: BigDecimal, base: BigDecimal, precision: Int) raises -> BigDecimal:
20662132
# Special cases
20672133
if x.sign:
20682134
raise Error(
2069-
"Error in log(): Cannot compute logarithm of a negative number"
2135+
ValueError(
2136+
message="Cannot compute logarithm of a negative number",
2137+
function="log()",
2138+
)
20702139
)
20712140
if x.coefficient.is_zero():
2072-
raise Error("Error in log(): Cannot compute logarithm of zero")
2141+
raise Error(
2142+
ValueError(
2143+
message="Cannot compute logarithm of zero", function="log()"
2144+
)
2145+
)
20732146

20742147
# Base validation
20752148
if base.sign:
2076-
raise Error("Error in log(): Cannot use a negative base")
2149+
raise Error(
2150+
ValueError(message="Cannot use a negative base", function="log()")
2151+
)
20772152
if base.coefficient.is_zero():
2078-
raise Error("Error in log(): Cannot use zero as a base")
2153+
raise Error(
2154+
ValueError(message="Cannot use zero as a base", function="log()")
2155+
)
20792156
if (
20802157
base.coefficient.number_of_digits() == base.scale + 1
20812158
and base.coefficient.words[-1] == 1
20822159
):
2083-
raise Error("Error in log(): Cannot use base 1 for logarithm")
2160+
raise Error(
2161+
ValueError(
2162+
message="Cannot use base 1 for logarithm", function="log()"
2163+
)
2164+
)
20842165

20852166
# Special cases
20862167
if (
@@ -2129,10 +2210,17 @@ def log10(x: BigDecimal, precision: Int) raises -> BigDecimal:
21292210
# Special cases
21302211
if x.sign:
21312212
raise Error(
2132-
"Error in log10(): Cannot compute logarithm of a negative number"
2213+
ValueError(
2214+
message="Cannot compute logarithm of a negative number",
2215+
function="log10()",
2216+
)
21332217
)
21342218
if x.coefficient.is_zero():
2135-
raise Error("Error in log10(): Cannot compute logarithm of zero")
2219+
raise Error(
2220+
ValueError(
2221+
message="Cannot compute logarithm of zero", function="log10()"
2222+
)
2223+
)
21362224

21372225
# Fast path: Powers of 10 are handled directly
21382226
if x.coefficient.is_power_of_10():

0 commit comments

Comments
 (0)