gbranden pushed a commit to branch master
in repository groff.
commit 17f18cc8cbedb6fcb83b531f253a288a19ec36e0
Author: G. Branden Robinson <[email protected]>
AuthorDate: Fri Aug 16 14:53:13 2024 -0500
[troff]: Fix Savannah #66001 (saturating arith).
* src/roff/troff/number.cpp (is_valid_term): When reading an overlong
integer literal, save the value corresponding to the longest valid
digit sequence, and use that if overflow occurs.
(scale): Demote "numeric overflow" errors to "integer value saturated"
warnings in category "range".
* src/roff/troff/hvunits.h (vunits::to_units)
(class vunits: operator +, operator -, operator *):
(hunits::to_units)
(class hunits: operator +, operator -, operator *):
* src/roff/troff/number.cpp (vunits::vunits, hunits::hunits): Impose
saturating arithmetic on overflowing operations. Demote overflow from
error, throwing warning in "range" category if overflow would occur,
and describe integer result as "saturated" rather than "wrapped".
* src/roff/groff/tests/arithmetic-works.sh: Enable warnings in "range"
category. Update test expectations. Drop check of operation that is
no longer meaningful with refactored arithmetic parsing.
* doc/groff.texi.in (Numeric Expressions):
* man/groff.7.man (Numeric expressions):
* NEWS: Document it.
Fixes <https://savannah.gnu.org/bugs/?66001>.
---
ChangeLog | 29 ++++++++++++++
NEWS | 4 ++
doc/groff.texi.in | 12 ++++--
man/groff.7.man | 11 ++++--
src/roff/groff/tests/arithmetic-works.sh | 50 +++++++++++++----------
src/roff/troff/hvunits.h | 26 ++++++------
src/roff/troff/number.cpp | 68 ++++++++++++++++++++------------
7 files changed, 135 insertions(+), 65 deletions(-)
diff --git a/ChangeLog b/ChangeLog
index dfd57971a..9959ac25a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,32 @@
+2024-08-16 G. Branden Robinson <[email protected]>
+
+ * src/roff/troff/number.cpp (is_valid_term): When reading an
+ overlong integer literal, save the value corresponding to the
+ longest valid digit sequence, and use that if overflow occurs.
+ (scale): Demote "numeric overflow" errors to "integer value
+ saturated" warnings in category "range".
+
+ * src/roff/troff/hvunits.h (vunits::to_units)
+ (class vunits: operator +, operator -, operator *):
+ (hunits::to_units)
+ (class hunits: operator +, operator -, operator *):
+ * src/roff/troff/number.cpp (vunits::vunits, hunits::hunits):
+ Impose saturating arithmetic on overflowing operations. Demote
+ overflow from error, throwing warning in "range" category if
+ overflow would occur, and describe integer result as "saturated"
+ rather than "wrapped".
+
+ * src/roff/groff/tests/arithmetic-works.sh: Enable warnings in
+ "range" category. Update test expectations. Drop check of
+ operation that is no longer meaningful with refactored
+ arithmetic parsing.
+
+ * doc/groff.texi.in (Numeric Expressions):
+ * man/groff.7.man (Numeric expressions):
+ * NEWS: Document it.
+
+ Fixes <https://savannah.gnu.org/bugs/?66001>.
+
2024-08-16 G. Branden Robinson <[email protected]>
* src/roff/troff/number.cpp (get_vunits, get_hunts, get_number)
diff --git a/NEWS b/NEWS
index ff3f839bc..1344eb4e6 100644
--- a/NEWS
+++ b/NEWS
@@ -39,6 +39,10 @@ o The `color`, `cp`, `linetabs`, and `vpt` requests now
interpret
with a register interpolation as an argument, the outcome agrees with
an `if` test of the register's value.
+o GNU troff now implements saturating rather than wrapping integer
+ arithmetic. Where before overflow would cause an error diagnostic,
+ the formatter now emits a warning diagnostic in the "range" category.
+
o The formatter no longer reports warnings in the "el" category. There
was only one; it threw a warning diagnostic if enabled (which it was
not by default) if it inferred an imbalance between `ie` and `el`
diff --git a/doc/groff.texi.in b/doc/groff.texi.in
index f51e66a35..208afe15f 100644
--- a/doc/groff.texi.in
+++ b/doc/groff.texi.in
@@ -6190,6 +6190,9 @@ used for computing results depends on the host machine
but is at least a
range of �2 billion.@footnote{If that's not enough, see the
@cite{groff_tmac@r{(5)}} man page for the @file{62bit.tmac} macro
package.}
+@cindex saturating arithmetic
+Arithmetic saturates rather than wrapping; if overflow would occur, GNU
+@command{troff} emits a warning in category @samp{range}.
@cindex arithmetic operators
@cindex operators, arithmetic
@@ -6209,8 +6212,12 @@ to their left and right; they are @code{+} (addition),
@code{-}
(subtraction), @code{*} (multiplication), @code{/} (truncating
division), and @code{%} (modulus). @dfn{Truncating division} rounds to
the integer nearer to zero, no matter how large the fractional portion.
-Overflow and division (or modulus) by zero are errors and abort
-evaluation of a numeric expression.
+@cindex division by zero
+@cindex modulus by zero
+@cindex zero, division and modulus by
+Division and modulus by zero are errors and abort evaluation of a
+numeric expression.
+
@cindex unary arithmetic operators
@cindex operators, unary arithmetic
@cindex negation
@@ -6219,7 +6226,6 @@ evaluation of a numeric expression.
@opindex +
@cindex @code{if} request, and the @samp{!} operator
@cindex @code{while} request, and the @samp{!} operator
-
Arithmetic unary operators operate on the numeric expression to their
right; they are @code{-} (negation) and @code{+} (assertion---for
completeness; it does nothing). The unary minus must often be used
diff --git a/man/groff.7.man b/man/groff.7.man
index b3c1ef163..1d3b759dd 100644
--- a/man/groff.7.man
+++ b/man/groff.7.man
@@ -976,6 +976,12 @@ for the
.I 62bit.tmac
macro package.)
.
+Arithmetic saturates rather than wrapping;
+if overflow would occur,
+.I @g@troff
+emits a warning in category
+.RB \%\[lq] range \[rq].
+.
.
.P
Arithmetic infix operators perform a function on the numeric expressions
@@ -997,9 +1003,8 @@ and
rounds to the integer nearer to zero,
no matter how large the fractional portion.
.
-Overflow and division
-(or modulus)
-by zero are errors and abort evaluation of a numeric expression.
+Division and modulus by zero
+are errors and abort evaluation of a numeric expression.
.
.
.P
diff --git a/src/roff/groff/tests/arithmetic-works.sh
b/src/roff/groff/tests/arithmetic-works.sh
index f7bb699ad..6cbcb81a1 100755
--- a/src/roff/groff/tests/arithmetic-works.sh
+++ b/src/roff/groff/tests/arithmetic-works.sh
@@ -57,13 +57,13 @@ input='.
#
# 2: .l=1512
#
-# 3: .l=24, .H=24
+# 3: .l=2147483640, .H=24
#
-# 4: .p=40, .V=40
+# 4: .p=2147483640, .V=40
#
# The blank lines are due to the `vs` increase.
-output=$(echo "$input" | "$groff" -T ascii)
+output=$(echo "$input" | "$groff" -w range -T ascii)
echo "$output"
echo "checking that vertical spacing is correctly incremented" >&2
@@ -72,11 +72,11 @@ echo "$output" | grep -Fqx '1: .v=80' || wail
echo "checking that line length is correctly incremented" >&2
echo "$output" | grep -Fqx '2: .l=1512' || wail
-echo "checking that setting huge line length does not overflow" >&2
-echo "$output" | grep -Fqx '3: .l=24, .H=24' || wail
+echo "checking that setting huge line length saturates" >&2
+echo "$output" | grep -Fqx '3: .l=2147483640, .H=24' || wail
-echo "checking that setting huge page length does not overflow" >&2
-echo "$output" | grep -Fqx '4: .p=40, .V=40' || wail
+echo "checking that setting huge page length saturates" >&2
+echo "$output" | grep -Fqx '4: .p=2147483640, .V=40' || wail
input='.
.ec @
@@ -91,7 +91,7 @@ c: @nc
d: @nd
.'
-output=$(echo "$input" | "$groff" -T ascii)
+output=$(echo "$input" | "$groff" -w range -T ascii)
echo "$output"
# The vunits and hunits constructors in src/roff/troff/number.cpp don't
@@ -132,28 +132,22 @@ input='.
5: (-1)*2147483647 -> @ni
.nr i -1
6: decr -2147483648 -> @ni
-.nr i -2147483648
-.nr i (-1)*@ni
-7: (-1)*(-2147483648) -> @ni
.'
-output=$(echo "$input" | "$groff" -T ascii)
+output=$(echo "$input" | "$groff" -w range -T ascii)
echo "$output"
-# 2147483647
echo "checking assignment of huge value to register" >&2
echo "$output" \
- | grep -Fqx '1: 99999999999999999999 -> 1410065407' || wail
+ | grep -Fqx '1: 99999999999999999999 -> 999999999' || wail
-# -2147483648
echo "checking assignment of huge negative value to register" >&2
echo "$output" \
- | grep -Fqx '2: (-99999999999999999999) -> -1410065407' || wail
+ | grep -Fqx '2: (-99999999999999999999) -> -999999999' || wail
echo "checking assignment of 2^31 - 1 to register" >&2
echo "$output" | grep -Fqx '3: assign 2147483647 -> 2147483647' || wail
-# -2147483648
echo "checking incrementation of register" >&2
echo "$output" | grep -Fqx '4: incr 2147483647 -> -2147483648' || wail
@@ -163,10 +157,24 @@ echo "$output" | grep -Fqx '5: (-1)*2147483647 ->
-2147483647' || wail
echo "checking decrementation of register" >&2
echo "$output" | grep -Fqx '6: decr -2147483648 -> -2147483648' || wail
-# 2147483647
-echo "checking negation of negatively register" >&2
-echo "$output" | grep -Fqx '7: (-1)*(-2147483648) -> 0' \
- || wail
+# A quirk that GNU troff arithmetic has is that one cannot directly
+# assign INT_MIN to a register, because the negative sign is parsed
+# separately from the digit sequence. (This is true even when the
+# negative sign is not intrepreted as a decrementation operator.)
+#
+# .nr a 2147483647
+# .tm a=\na
+# a=2147483647
+# .nr a \na*-1
+# .tm a=\na
+# a=-2147483647
+# .nr a -1
+# .tm a=\na
+# a=-2147483648
+# .nr a \na*-1
+# troff:<standard input>:7: warning: integer value saturated
+#
+# This seems okay to GBR.
test -z "$fail"
diff --git a/src/roff/troff/hvunits.h b/src/roff/troff/hvunits.h
index a6f6fb456..63de9c405 100644
--- a/src/roff/troff/hvunits.h
+++ b/src/roff/troff/hvunits.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 1989-2020 Free Software Foundation, Inc.
+/* Copyright (C) 1989-2024 Free Software Foundation, Inc.
Written by James Clark ([email protected])
This file is part of groff.
@@ -93,7 +93,7 @@ inline units vunits::to_units()
{
units r;
if (ckd_mul(&r, n, vresolution))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
return r;
}
@@ -107,7 +107,7 @@ inline vunits operator +(const vunits & x, const vunits & y)
vunits r;
r = x;
if (ckd_add(&r.n, r.n, y.n))
- error("integer addition wrapped");
+ warning(WARN_RANGE, "integer addition saturated");
return r;
}
@@ -116,7 +116,7 @@ inline vunits operator -(const vunits & x, const vunits & y)
vunits r;
r = x;
if (ckd_sub(&r.n, r.n, y.n))
- error("integer subtraction wrapped");
+ warning(WARN_RANGE, "integer subtraction saturated");
return r;
}
@@ -125,7 +125,7 @@ inline vunits operator -(const vunits & x)
vunits r;
// Why? Consider -(INT_MIN) in two's complement.
if (ckd_mul(&r.n, x.n, -1))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
return r;
}
@@ -147,7 +147,7 @@ inline vunits operator *(const vunits & x, int n)
vunits r;
r = x;
if (ckd_mul(&r.n, x.n, n))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
return r;
}
@@ -156,7 +156,7 @@ inline vunits operator *(int n, const vunits & x)
vunits r;
r = x;
if (ckd_mul(&r.n, n, x.n))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
return r;
}
@@ -210,7 +210,7 @@ inline units hunits::to_units()
{
units r;
if (ckd_mul(&r, n, hresolution))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
return r;
}
@@ -224,7 +224,7 @@ inline hunits operator +(const hunits & x, const hunits & y)
hunits r;
r = x;
if (ckd_add(&r.n, r.n, y.n))
- error("integer addition wrapped");
+ warning(WARN_RANGE, "integer addition saturated");
return r;
}
@@ -233,7 +233,7 @@ inline hunits operator -(const hunits & x, const hunits & y)
hunits r;
r = x;
if (ckd_sub(&r.n, r.n, y.n))
- error("integer subtraction wrapped");
+ warning(WARN_RANGE, "integer subtraction saturated");
return r;
}
@@ -243,7 +243,7 @@ inline hunits operator -(const hunits & x)
r = x;
// Why? Consider -(INT_MIN) in two's complement.
if (ckd_mul(&r.n, x.n, -1))
- error("integer subtraction wrapped");
+ warning(WARN_RANGE, "integer subtraction saturated");
return r;
}
@@ -265,7 +265,7 @@ inline hunits operator *(const hunits & x, int n)
hunits r;
r = x;
if (ckd_mul(&r.n, x.n, n))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
return r;
}
@@ -274,7 +274,7 @@ inline hunits operator *(int n, const hunits & x)
hunits r;
r = x;
if (ckd_mul(&r.n, x.n, n))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
return r;
}
diff --git a/src/roff/troff/number.cpp b/src/roff/troff/number.cpp
index bc92e955d..1ece5dc1d 100644
--- a/src/roff/troff/number.cpp
+++ b/src/roff/troff/number.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 1989-2020 Free Software Foundation, Inc.
+/* Copyright (C) 1989-2024 Free Software Foundation, Inc.
Written by James Clark ([email protected])
This file is part of groff.
@@ -129,12 +129,12 @@ bool get_vunits(vunits *res, unsigned char si, vunits
prev_value)
break;
case INCREMENT:
if (ckd_add(&i, prev_value.to_units(), v))
- error("integer incrementation wrapped");
+ warning(WARN_RANGE, "integer incrementation saturated");
*res = i;
break;
case DECREMENT:
if (ckd_sub(&i, prev_value.to_units(), v))
- error("integer decrementation wrapped");
+ warning(WARN_RANGE, "integer decrementation saturated");
*res = i;
break;
default:
@@ -157,12 +157,12 @@ bool get_hunits(hunits *res, unsigned char si, hunits
prev_value)
break;
case INCREMENT:
if (ckd_add(&i, prev_value.to_units(), h))
- error("integer incrementation wrapped");
+ warning(WARN_RANGE, "integer incrementation saturated");
*res = i;
break;
case DECREMENT:
if (ckd_sub(&i, prev_value.to_units(), h))
- error("integer decrementation wrapped");
+ warning(WARN_RANGE, "integer decrementation saturated");
*res = i;
break;
default:
@@ -182,11 +182,11 @@ bool get_number(units *res, unsigned char si, units
prev_value)
break;
case INCREMENT:
if (ckd_add(res, prev_value, u))
- error("integer incrementation wrapped");
+ warning(WARN_RANGE, "integer incrementation saturated");
break;
case DECREMENT:
if (ckd_sub(res, prev_value, u))
- error("integer decrementation wrapped");
+ warning(WARN_RANGE, "integer decrementation saturated");
break;
default:
assert(0 == "unhandled case in get_number()");
@@ -205,11 +205,11 @@ bool get_integer(int *res, int prev_value)
break;
case INCREMENT:
if (ckd_add(res, prev_value, i))
- error("integer incrementation wrapped");
+ warning(WARN_RANGE, "integer incrementation saturated");
break;
case DECREMENT:
if (ckd_sub(res, prev_value, i))
- error("integer decrementation wrapped");
+ warning(WARN_RANGE, "integer decrementation saturated");
break;
default:
assert(0 == "unhandled case in get_integer()");
@@ -351,19 +351,19 @@ static bool is_valid_expression(units *u, int
scaling_unit,
break;
case '+':
if (ckd_add(u, *u, u2)) {
- error("integer addition wrapped");
+ warning(WARN_RANGE, "integer addition saturated");
return false;
}
break;
case '-':
if (ckd_sub(u, *u, u2)) {
- error("integer subtraction wrapped");
+ warning(WARN_RANGE, "integer subtraction saturated");
return false;
}
break;
case '*':
if (ckd_mul(u, *u, u2)) {
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
return false;
}
break;
@@ -393,6 +393,7 @@ static bool is_valid_term(units *u, int scaling_unit,
{
bool is_negative = false;
bool is_overflowing = false;
+ units saved_u = 0; // for use when reading an overlong number
for (;;)
if (is_parenthesized && tok.is_space())
tok.next();
@@ -416,10 +417,9 @@ static bool is_valid_term(units *u, int scaling_unit,
position = (scaling_unit == 'v'
? curdiv->get_vertical_position().to_units()
: curenv->get_input_line_position().to_units());
- // We don't permit integer wraparound with this operator.
if (ckd_sub(&tmp, *u, position)) {
- error("numeric overflow");
- return false;
+ tmp = INT_MAX;
+ warning(WARN_RANGE, "integer value saturated");
}
*u = tmp;
if (is_negative)
@@ -466,7 +466,7 @@ static bool is_valid_term(units *u, int scaling_unit,
if (is_negative) {
// Why? Consider -(INT_MIN) in two's complement.
if (ckd_mul(u, *u, -1))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
}
return true;
case '.':
@@ -486,16 +486,19 @@ static bool is_valid_term(units *u, int scaling_unit,
do {
// If wrapping, don't `break`; eat and discard further digits.
if (!is_overflowing) {
+ saved_u = *u;
if (ckd_mul(u, *u, 10))
is_overflowing = true;
if (ckd_add(u, *u, c - '0'))
is_overflowing = true;
+ if (is_overflowing)
+ *u = saved_u;
}
tok.next();
c = tok.ch();
} while (csdigit(c));
if (is_overflowing)
- error("integer value wrapped");
+ warning(WARN_RANGE, "integer value saturated");
break;
case '/':
case '*':
@@ -623,7 +626,7 @@ static bool is_valid_term(units *u, int scaling_unit,
tok.next();
if (is_negative) {
if (ckd_mul(u, *u, -1))
- error("integer multiplication wrapped");
+ warning(WARN_RANGE, "integer multiplication saturated");
}
return true;
}
@@ -642,13 +645,12 @@ units scale(units n, units x, units y)
return (n * x) / y;
}
double res = n * double(x) / double(y);
- // We don't implement integer wraparound when scaling.
if (res > INT_MAX) {
- error("numeric overflow");
+ warning(WARN_RANGE, "integer value saturated");
return INT_MAX;
}
else if (res < INT_MIN) {
- error("numeric overflow");
+ warning(WARN_RANGE, "integer value saturated");
return INT_MIN;
}
return int(res);
@@ -671,9 +673,17 @@ vunits::vunits(units x)
if (ckd_add(&n, x, vcrement))
is_overflowing = true;
}
+ if (is_overflowing) {
+ if (x < 0) {
+ warning(WARN_RANGE, "integer value saturated");
+ n = INT_MIN;
+ }
+ else {
+ warning(WARN_RANGE, "integer value saturated");
+ n = INT_MAX;
+ }
+ }
n /= vresolution;
- if (is_overflowing)
- error("integer addition wrapped");
}
}
@@ -694,9 +704,17 @@ hunits::hunits(units x)
if (ckd_add(&n, x, hcrement))
is_overflowing = true;
}
+ if (is_overflowing) {
+ if (x < 0) {
+ warning(WARN_RANGE, "integer value saturated");
+ n = INT_MIN;
+ }
+ else {
+ warning(WARN_RANGE, "integer value saturated");
+ n = INT_MAX;
+ }
+ }
n /= hresolution;
- if (is_overflowing)
- error("integer addition wrapped");
}
}
_______________________________________________
Groff-commit mailing list
[email protected]
https://lists.gnu.org/mailman/listinfo/groff-commit