1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2024-09-19 10:50:24 -04:00

Merge pull request #11188 from lf-/jade/kill-int-overflow

Ban integer overflow in the Nix language
This commit is contained in:
Robert Hensing 2024-08-11 04:24:16 +02:00 committed by GitHub
commit 18485d2d53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 707 additions and 81 deletions

View file

@ -0,0 +1,21 @@
---
synopsis: Define integer overflow in the Nix language as an error
issues: [10968]
prs: [11188]
---
Previously, integer overflow in the Nix language invoked C++ level signed overflow, which was undefined behaviour, but *usually* manifested as wrapping around on overflow.
Since prior to the public release of Lix, Lix had C++ signed overflow defined to crash the process and nobody noticed this having accidentally removed overflow from the Nix language for three months until it was caught by fiddling around.
Given the significant body of actual Nix code that has been evaluated by Lix in that time, it does not appear that nixpkgs or much of importance depends on integer overflow, so it appears safe to turn into an error.
Some other overflows were fixed:
- `builtins.fromJSON` of values greater than the maximum representable value in a signed 64-bit integer will generate an error.
- `nixConfig` in flakes will no longer accept negative values for configuration options.
Integer overflow now looks like the following:
```
$ nix eval --expr '9223372036854775807 + 1'
error: integer overflow in adding 9223372036854775807 + 1
```

View file

@ -67,8 +67,12 @@ After evaluating *attrset* and *attrpath*, the computational complexity is O(log
## Arithmetic ## Arithmetic
Numbers are type-compatible: Numbers will retain their type unless mixed with other numeric types:
Pure integer operations will always return integers, whereas any operation involving at least one floating point number return a floating point number. Pure integer operations will always return integers, whereas any operation involving at least one floating point number returns a floating point number.
Evaluation of the following numeric operations throws an evaluation error:
- Division by zero
- Integer overflow, that is, any operation yielding a result outside of the representable range of [Nix language integers](./syntax.md#number-literal)
See also [Comparison] and [Equality]. See also [Comparison] and [Equality].

View file

@ -15,6 +15,13 @@ See [String literals](string-literals.md).
Numbers, which can be *integers* (like `123`) or *floating point* Numbers, which can be *integers* (like `123`) or *floating point*
(like `123.43` or `.27e13`). (like `123.43` or `.27e13`).
Integers in the Nix language are 64-bit [two's complement] signed integers, with a range of -9223372036854775808 to 9223372036854775807, inclusive.
[two's complement]: https://en.wikipedia.org/wiki/Two%27s_complement
Note that negative numeric literals are actually parsed as unary negation of positive numeric literals.
This means that the minimum integer `-9223372036854775808` cannot be written as-is as a literal, since the positive number `9223372036854775808` is one past the maximum range.
See [arithmetic] and [comparison] operators for semantics. See [arithmetic] and [comparison] operators for semantics.
[arithmetic]: ./operators.md#arithmetic [arithmetic]: ./operators.md#arithmetic

View file

@ -104,12 +104,12 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths()
auto drvPath = attr->forceDerivation(); auto drvPath = attr->forceDerivation();
std::optional<NixInt> priority; std::optional<NixInt::Inner> priority;
if (attr->maybeGetAttr(state->sOutputSpecified)) { if (attr->maybeGetAttr(state->sOutputSpecified)) {
} else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) { } else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) {
if (auto aPriority = aMeta->maybeGetAttr("priority")) if (auto aPriority = aMeta->maybeGetAttr("priority"))
priority = aPriority->getInt(); priority = aPriority->getInt().value;
} }
return {{ return {{

View file

@ -40,7 +40,7 @@ struct ExtraPathInfoValue : ExtraPathInfo
/** /**
* An optional priority for use with "build envs". See Package * An optional priority for use with "build envs". See Package
*/ */
std::optional<NixInt> priority; std::optional<NixInt::Inner> priority;
/** /**
* The attribute path associated with this value. The idea is * The attribute path associated with this value. The idea is

View file

@ -306,7 +306,7 @@ int64_t nix_get_int(nix_c_context * context, const nix_value * value)
try { try {
auto & v = check_value_in(value); auto & v = check_value_in(value);
assert(v.type() == nix::nInt); assert(v.type() == nix::nInt);
return v.integer(); return v.integer().value;
} }
NIXC_CATCH_ERRS_RES(0); NIXC_CATCH_ERRS_RES(0);
} }

View file

@ -328,7 +328,7 @@ struct AttrDb
case AttrType::Bool: case AttrType::Bool:
return {{rowId, queryAttribute.getInt(2) != 0}}; return {{rowId, queryAttribute.getInt(2) != 0}};
case AttrType::Int: case AttrType::Int:
return {{rowId, int_t{queryAttribute.getInt(2)}}}; return {{rowId, int_t{NixInt{queryAttribute.getInt(2)}}}};
case AttrType::ListOfStrings: case AttrType::ListOfStrings:
return {{rowId, tokenizeString<std::vector<std::string>>(queryAttribute.getStr(2), "\t")}}; return {{rowId, tokenizeString<std::vector<std::string>>(queryAttribute.getStr(2), "\t")}};
case AttrType::Missing: case AttrType::Missing:
@ -471,7 +471,7 @@ Value & AttrCursor::forceValue()
else if (v.type() == nBool) else if (v.type() == nBool)
cachedValue = {root->db->setBool(getKey(), v.boolean()), v.boolean()}; cachedValue = {root->db->setBool(getKey(), v.boolean()), v.boolean()};
else if (v.type() == nInt) else if (v.type() == nInt)
cachedValue = {root->db->setInt(getKey(), v.integer()), int_t{v.integer()}}; cachedValue = {root->db->setInt(getKey(), v.integer().value), int_t{v.integer()}};
else if (v.type() == nAttrs) else if (v.type() == nAttrs)
; // FIXME: do something? ; // FIXME: do something?
else else

View file

@ -1979,7 +1979,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
NixStringContext context; NixStringContext context;
std::vector<BackedStringView> s; std::vector<BackedStringView> s;
size_t sSize = 0; size_t sSize = 0;
NixInt n = 0; NixInt n{0};
NixFloat nf = 0; NixFloat nf = 0;
bool first = !forceString; bool first = !forceString;
@ -2023,17 +2023,22 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
if (firstType == nInt) { if (firstType == nInt) {
if (vTmp.type() == nInt) { if (vTmp.type() == nInt) {
n += vTmp.integer(); auto newN = n + vTmp.integer();
if (auto checked = newN.valueChecked(); checked.has_value()) {
n = NixInt(*checked);
} else {
state.error<EvalError>("integer overflow in adding %1% + %2%", n, vTmp.integer()).atPos(i_pos).debugThrow();
}
} else if (vTmp.type() == nFloat) { } else if (vTmp.type() == nFloat) {
// Upgrade the type from int to float; // Upgrade the type from int to float;
firstType = nFloat; firstType = nFloat;
nf = n; nf = n.value;
nf += vTmp.fpoint(); nf += vTmp.fpoint();
} else } else
state.error<EvalError>("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow(); state.error<EvalError>("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow();
} else if (firstType == nFloat) { } else if (firstType == nFloat) {
if (vTmp.type() == nInt) { if (vTmp.type() == nInt) {
nf += vTmp.integer(); nf += vTmp.integer().value;
} else if (vTmp.type() == nFloat) { } else if (vTmp.type() == nFloat) {
nf += vTmp.fpoint(); nf += vTmp.fpoint();
} else } else
@ -2158,7 +2163,7 @@ NixFloat EvalState::forceFloat(Value & v, const PosIdx pos, std::string_view err
try { try {
forceValue(v, pos); forceValue(v, pos);
if (v.type() == nInt) if (v.type() == nInt)
return v.integer(); return v.integer().value;
else if (v.type() != nFloat) else if (v.type() != nFloat)
error<TypeError>( error<TypeError>(
"expected a float but found %1%: %2%", "expected a float but found %1%: %2%",
@ -2345,7 +2350,7 @@ BackedStringView EvalState::coerceToString(
shell scripting convenience, just like `null'. */ shell scripting convenience, just like `null'. */
if (v.type() == nBool && v.boolean()) return "1"; if (v.type() == nBool && v.boolean()) return "1";
if (v.type() == nBool && !v.boolean()) return ""; if (v.type() == nBool && !v.boolean()) return "";
if (v.type() == nInt) return std::to_string(v.integer()); if (v.type() == nInt) return std::to_string(v.integer().value);
if (v.type() == nFloat) return std::to_string(v.fpoint()); if (v.type() == nFloat) return std::to_string(v.fpoint());
if (v.type() == nNull) return ""; if (v.type() == nNull) return "";
@ -2728,9 +2733,9 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
// Special case type-compatibility between float and int // Special case type-compatibility between float and int
if (v1.type() == nInt && v2.type() == nFloat) if (v1.type() == nInt && v2.type() == nFloat)
return v1.integer() == v2.fpoint(); return v1.integer().value == v2.fpoint();
if (v1.type() == nFloat && v2.type() == nInt) if (v1.type() == nFloat && v2.type() == nInt)
return v1.fpoint() == v2.integer(); return v1.fpoint() == v2.integer().value;
// All other types are not compatible with each other. // All other types are not compatible with each other.
if (v1.type() != v2.type()) return false; if (v1.type() != v2.type()) return false;

View file

@ -246,8 +246,8 @@ NixInt PackageInfo::queryMetaInt(const std::string & name, NixInt def)
if (v->type() == nString) { if (v->type() == nString) {
/* Backwards compatibility with before we had support for /* Backwards compatibility with before we had support for
integer meta fields. */ integer meta fields. */
if (auto n = string2Int<NixInt>(v->c_str())) if (auto n = string2Int<NixInt::Inner>(v->c_str()))
return *n; return NixInt{*n};
} }
return def; return def;
} }

View file

@ -2,6 +2,7 @@
#include "value.hh" #include "value.hh"
#include "eval.hh" #include "eval.hh"
#include <limits>
#include <variant> #include <variant>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@ -101,8 +102,12 @@ public:
return true; return true;
} }
bool number_unsigned(number_unsigned_t val) override bool number_unsigned(number_unsigned_t val_) override
{ {
if (val_ > std::numeric_limits<NixInt::Inner>::max()) {
throw Error("unsigned json number %1% outside of Nix integer range", val_);
}
NixInt::Inner val = val_;
rs->value(state).mkInt(val); rs->value(state).mkInt(val);
rs->add(); rs->add();
return true; return true;

View file

@ -138,7 +138,7 @@ or { return OR_KW; }
{INT} { errno = 0; {INT} { errno = 0;
std::optional<int64_t> numMay = string2Int<int64_t>(yytext); std::optional<int64_t> numMay = string2Int<int64_t>(yytext);
if (numMay.has_value()) { if (numMay.has_value()) {
yylval->n = *numMay; yylval->n = NixInt{*numMay};
} else { } else {
throw ParseError(ErrorInfo{ throw ParseError(ErrorInfo{
.msg = HintFmt("invalid integer '%1%'", yytext), .msg = HintFmt("invalid integer '%1%'", yytext),

View file

@ -107,6 +107,7 @@ struct ExprInt : Expr
{ {
Value v; Value v;
ExprInt(NixInt n) { v.mkInt(n); }; ExprInt(NixInt n) { v.mkInt(n); };
ExprInt(NixInt::Inner n) { v.mkInt(n); };
Value * maybeThunk(EvalState & state, Env & env) override; Value * maybeThunk(EvalState & state, Env & env) override;
COMMON_METHODS COMMON_METHODS
}; };

View file

@ -587,9 +587,9 @@ struct CompareValues
{ {
try { try {
if (v1->type() == nFloat && v2->type() == nInt) if (v1->type() == nFloat && v2->type() == nInt)
return v1->fpoint() < v2->integer(); return v1->fpoint() < v2->integer().value;
if (v1->type() == nInt && v2->type() == nFloat) if (v1->type() == nInt && v2->type() == nFloat)
return v1->integer() < v2->fpoint(); return v1->integer().value < v2->fpoint();
if (v1->type() != v2->type()) if (v1->type() != v2->type())
state.error<EvalError>("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow(); state.error<EvalError>("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow();
// Allow selecting a subset of enum values // Allow selecting a subset of enum values
@ -2762,13 +2762,13 @@ static struct LazyPosAcessors {
PrimOp primop_lineOfPos{ PrimOp primop_lineOfPos{
.arity = 1, .arity = 1,
.fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) { .fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) {
v.mkInt(state.positions[PosIdx(args[0]->integer())].line); v.mkInt(state.positions[PosIdx(args[0]->integer().value)].line);
} }
}; };
PrimOp primop_columnOfPos{ PrimOp primop_columnOfPos{
.arity = 1, .arity = 1,
.fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) { .fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) {
v.mkInt(state.positions[PosIdx(args[0]->integer())].column); v.mkInt(state.positions[PosIdx(args[0]->integer().value)].column);
} }
}; };
@ -3244,7 +3244,8 @@ static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Val
/* Return the n-1'th element of a list. */ /* Return the n-1'th element of a list. */
static void prim_elemAt(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_elemAt(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
elemAt(state, pos, *args[0], state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.elemAt"), v); NixInt::Inner elem = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.elemAt").value;
elemAt(state, pos, *args[0], elem, v);
} }
static RegisterPrimOp primop_elemAt({ static RegisterPrimOp primop_elemAt({
@ -3538,10 +3539,12 @@ static RegisterPrimOp primop_all({
static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
auto len = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.genList"); auto len_ = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.genList").value;
if (len < 0) if (len_ < 0)
state.error<EvalError>("cannot create list of size %1%", len).atPos(pos).debugThrow(); state.error<EvalError>("cannot create list of size %1%", len_).atPos(pos).debugThrow();
size_t len = size_t(len_);
// More strict than striclty (!) necessary, but acceptable // More strict than striclty (!) necessary, but acceptable
// as evaluating map without accessing any values makes little sense. // as evaluating map without accessing any values makes little sense.
@ -3798,9 +3801,17 @@ static void prim_add(EvalState & state, const PosIdx pos, Value * * args, Value
if (args[0]->type() == nFloat || args[1]->type() == nFloat) if (args[0]->type() == nFloat || args[1]->type() == nFloat)
v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the addition") v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the addition")
+ state.forceFloat(*args[1], pos, "while evaluating the second argument of the addition")); + state.forceFloat(*args[1], pos, "while evaluating the second argument of the addition"));
else else {
v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the addition") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument of the addition");
+ state.forceInt(*args[1], pos, "while evaluating the second argument of the addition")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument of the addition");
auto result_ = i1 + i2;
if (auto result = result_.valueChecked(); result.has_value()) {
v.mkInt(*result);
} else {
state.error<EvalError>("integer overflow in adding %1% + %2%", i1, i2).atPos(pos).debugThrow();
}
}
} }
static RegisterPrimOp primop_add({ static RegisterPrimOp primop_add({
@ -3819,9 +3830,18 @@ static void prim_sub(EvalState & state, const PosIdx pos, Value * * args, Value
if (args[0]->type() == nFloat || args[1]->type() == nFloat) if (args[0]->type() == nFloat || args[1]->type() == nFloat)
v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the subtraction") v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the subtraction")
- state.forceFloat(*args[1], pos, "while evaluating the second argument of the subtraction")); - state.forceFloat(*args[1], pos, "while evaluating the second argument of the subtraction"));
else else {
v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the subtraction") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument of the subtraction");
- state.forceInt(*args[1], pos, "while evaluating the second argument of the subtraction")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument of the subtraction");
auto result_ = i1 - i2;
if (auto result = result_.valueChecked(); result.has_value()) {
v.mkInt(*result);
} else {
state.error<EvalError>("integer overflow in subtracting %1% - %2%", i1, i2).atPos(pos).debugThrow();
}
}
} }
static RegisterPrimOp primop_sub({ static RegisterPrimOp primop_sub({
@ -3840,9 +3860,18 @@ static void prim_mul(EvalState & state, const PosIdx pos, Value * * args, Value
if (args[0]->type() == nFloat || args[1]->type() == nFloat) if (args[0]->type() == nFloat || args[1]->type() == nFloat)
v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first of the multiplication") v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first of the multiplication")
* state.forceFloat(*args[1], pos, "while evaluating the second argument of the multiplication")); * state.forceFloat(*args[1], pos, "while evaluating the second argument of the multiplication"));
else else {
v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the multiplication") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument of the multiplication");
* state.forceInt(*args[1], pos, "while evaluating the second argument of the multiplication")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument of the multiplication");
auto result_ = i1 * i2;
if (auto result = result_.valueChecked(); result.has_value()) {
v.mkInt(*result);
} else {
state.error<EvalError>("integer overflow in multiplying %1% * %2%", i1, i2).atPos(pos).debugThrow();
}
}
} }
static RegisterPrimOp primop_mul({ static RegisterPrimOp primop_mul({
@ -3869,10 +3898,12 @@ static void prim_div(EvalState & state, const PosIdx pos, Value * * args, Value
NixInt i1 = state.forceInt(*args[0], pos, "while evaluating the first operand of the division"); NixInt i1 = state.forceInt(*args[0], pos, "while evaluating the first operand of the division");
NixInt i2 = state.forceInt(*args[1], pos, "while evaluating the second operand of the division"); NixInt i2 = state.forceInt(*args[1], pos, "while evaluating the second operand of the division");
/* Avoid division overflow as it might raise SIGFPE. */ /* Avoid division overflow as it might raise SIGFPE. */
if (i1 == std::numeric_limits<NixInt>::min() && i2 == -1) auto result_ = i1 / i2;
state.error<EvalError>("overflow in integer division").atPos(pos).debugThrow(); if (auto result = result_.valueChecked(); result.has_value()) {
v.mkInt(*result);
v.mkInt(i1 / i2); } else {
state.error<EvalError>("integer overflow in dividing %1% / %2%", i1, i2).atPos(pos).debugThrow();
}
} }
} }
@ -3887,8 +3918,9 @@ static RegisterPrimOp primop_div({
static void prim_bitAnd(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_bitAnd(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitAnd") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitAnd");
& state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitAnd")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitAnd");
v.mkInt(i1.value & i2.value);
} }
static RegisterPrimOp primop_bitAnd({ static RegisterPrimOp primop_bitAnd({
@ -3902,8 +3934,10 @@ static RegisterPrimOp primop_bitAnd({
static void prim_bitOr(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_bitOr(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitOr") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitOr");
| state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitOr")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitOr");
v.mkInt(i1.value | i2.value);
} }
static RegisterPrimOp primop_bitOr({ static RegisterPrimOp primop_bitOr({
@ -3917,8 +3951,10 @@ static RegisterPrimOp primop_bitOr({
static void prim_bitXor(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_bitXor(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitXor") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitXor");
^ state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitXor")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitXor");
v.mkInt(i1.value ^ i2.value);
} }
static RegisterPrimOp primop_bitXor({ static RegisterPrimOp primop_bitXor({
@ -3998,13 +4034,19 @@ static RegisterPrimOp primop_toString({
non-negative. */ non-negative. */
static void prim_substring(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_substring(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
int start = state.forceInt(*args[0], pos, "while evaluating the first argument (the start offset) passed to builtins.substring"); NixInt::Inner start = state.forceInt(*args[0], pos, "while evaluating the first argument (the start offset) passed to builtins.substring").value;
if (start < 0) if (start < 0)
state.error<EvalError>("negative start position in 'substring'").atPos(pos).debugThrow(); state.error<EvalError>("negative start position in 'substring'").atPos(pos).debugThrow();
int len = state.forceInt(*args[1], pos, "while evaluating the second argument (the substring length) passed to builtins.substring"); NixInt::Inner len = state.forceInt(*args[1], pos, "while evaluating the second argument (the substring length) passed to builtins.substring").value;
// Negative length may be idiomatically passed to builtins.substring to get
// the tail of the string.
if (len < 0) {
len = std::numeric_limits<NixInt::Inner>::max();
}
// Special-case on empty substring to avoid O(n) strlen // Special-case on empty substring to avoid O(n) strlen
// This allows for the use of empty substrings to efficently capture string context // This allows for the use of empty substrings to efficently capture string context
@ -4047,7 +4089,7 @@ static void prim_stringLength(EvalState & state, const PosIdx pos, Value * * arg
{ {
NixStringContext context; NixStringContext context;
auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.stringLength"); auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.stringLength");
v.mkInt(s->size()); v.mkInt(NixInt::Inner(s->size()));
} }
static RegisterPrimOp primop_stringLength({ static RegisterPrimOp primop_stringLength({
@ -4531,7 +4573,8 @@ static void prim_compareVersions(EvalState & state, const PosIdx pos, Value * *
{ {
auto version1 = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.compareVersions"); auto version1 = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.compareVersions");
auto version2 = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.compareVersions"); auto version2 = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.compareVersions");
v.mkInt(compareVersions(version1, version2)); auto result = compareVersions(version1, version2);
v.mkInt(result < 0 ? -1 : result > 0 ? 1 : 0);
} }
static RegisterPrimOp primop_compareVersions({ static RegisterPrimOp primop_compareVersions({

View file

@ -122,9 +122,15 @@ static void fetchTree(
} }
else if (attr.value->type() == nBool) else if (attr.value->type() == nBool)
attrs.emplace(state.symbols[attr.name], Explicit<bool>{attr.value->boolean()}); attrs.emplace(state.symbols[attr.name], Explicit<bool>{attr.value->boolean()});
else if (attr.value->type() == nInt) else if (attr.value->type() == nInt) {
attrs.emplace(state.symbols[attr.name], uint64_t(attr.value->integer())); auto intValue = attr.value->integer().value;
else if (state.symbols[attr.name] == "publicKeys") {
if (intValue < 0) {
state.error<EvalError>("negative value given for fetchTree attr %1%: %2%", state.symbols[attr.name], intValue).atPos(pos).debugThrow();
}
attrs.emplace(state.symbols[attr.name], uint64_t(intValue));
} else if (state.symbols[attr.name] == "publicKeys") {
experimentalFeatureSettings.require(Xp::VerifiedFetches); experimentalFeatureSettings.require(Xp::VerifiedFetches);
attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, pos, context).dump()); attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, pos, context).dump());
} }

View file

@ -22,7 +22,7 @@ json printValueAsJSON(EvalState & state, bool strict,
switch (v.type()) { switch (v.type()) {
case nInt: case nInt:
out = v.integer(); out = v.integer().value;
break; break;
case nBool: case nBool:

View file

@ -8,6 +8,7 @@
#include "value/context.hh" #include "value/context.hh"
#include "source-path.hh" #include "source-path.hh"
#include "print-options.hh" #include "print-options.hh"
#include "checked-arithmetic.hh"
#if HAVE_BOEHMGC #if HAVE_BOEHMGC
#include <gc/gc_allocator.h> #include <gc/gc_allocator.h>
@ -73,8 +74,8 @@ class EvalState;
class XMLWriter; class XMLWriter;
class Printer; class Printer;
typedef int64_t NixInt; using NixInt = checked::Checked<int64_t>;
typedef double NixFloat; using NixFloat = double;
/** /**
* External values must descend from ExternalValueBase, so that * External values must descend from ExternalValueBase, so that
@ -304,6 +305,11 @@ public:
return internalType != tUninitialized; return internalType != tUninitialized;
} }
inline void mkInt(NixInt::Inner n)
{
mkInt(NixInt{n});
}
inline void mkInt(NixInt n) inline void mkInt(NixInt n)
{ {
finishValue(tInt, { .integer = n }); finishValue(tInt, { .integer = n });

View file

@ -140,9 +140,16 @@ static FlakeInput parseFlakeInput(EvalState & state,
case nBool: case nBool:
attrs.emplace(state.symbols[attr.name], Explicit<bool> { attr.value->boolean() }); attrs.emplace(state.symbols[attr.name], Explicit<bool> { attr.value->boolean() });
break; break;
case nInt: case nInt: {
attrs.emplace(state.symbols[attr.name], (long unsigned int) attr.value->integer()); auto intValue = attr.value->integer().value;
if (intValue < 0) {
state.error<EvalError>("negative value given for flake input attribute %1%: %2%", state.symbols[attr.name], intValue).debugThrow();
}
attrs.emplace(state.symbols[attr.name], uint64_t(intValue));
break; break;
}
default: default:
if (attr.name == state.symbols.create("publicKeys")) { if (attr.name == state.symbols.create("publicKeys")) {
experimentalFeatureSettings.require(Xp::VerifiedFetches); experimentalFeatureSettings.require(Xp::VerifiedFetches);
@ -272,7 +279,7 @@ static Flake readFlake(
else if (setting.value->type() == nInt) else if (setting.value->type() == nInt)
flake.config.settings.emplace( flake.config.settings.emplace(
state.symbols[setting.name], state.symbols[setting.name],
state.forceInt(*setting.value, setting.pos, "")); state.forceInt(*setting.value, setting.pos, "").value);
else if (setting.value->type() == nBool) else if (setting.value->type() == nBool)
flake.config.settings.emplace( flake.config.settings.emplace(
state.symbols[setting.name], state.symbols[setting.name],
@ -904,8 +911,13 @@ static void prim_flakeRefToString(
for (const auto & attr : *args[0]->attrs()) { for (const auto & attr : *args[0]->attrs()) {
auto t = attr.value->type(); auto t = attr.value->type();
if (t == nInt) { if (t == nInt) {
attrs.emplace(state.symbols[attr.name], auto intValue = attr.value->integer().value;
(uint64_t) attr.value->integer());
if (intValue < 0) {
state.error<EvalError>("negative value given for flake ref attr %1%: %2%", state.symbols[attr.name], intValue).atPos(pos).debugThrow();
}
attrs.emplace(state.symbols[attr.name], uint64_t(intValue));
} else if (t == nBool) { } else if (t == nBool) {
attrs.emplace(state.symbols[attr.name], attrs.emplace(state.symbols[attr.name],
Explicit<bool> { attr.value->boolean() }); Explicit<bool> { attr.value->boolean() });

View file

@ -1131,7 +1131,10 @@ public:
)"}; )"};
Setting<uint64_t> maxFree{ Setting<uint64_t> maxFree{
this, std::numeric_limits<uint64_t>::max(), "max-free", // n.b. this is deliberately int64 max rather than uint64 max because
// this goes through the Nix language JSON parser and thus needs to be
// representable in Nix language integers.
this, std::numeric_limits<int64_t>::max(), "max-free",
R"( R"(
When a garbage collection is triggered by the `min-free` option, it When a garbage collection is triggered by the `min-free` option, it
stops as soon as `max-free` bytes are available. The default is stops as soon as `max-free` bytes are available. The default is
@ -1221,7 +1224,10 @@ public:
Setting<uint64_t> warnLargePathThreshold{ Setting<uint64_t> warnLargePathThreshold{
this, this,
std::numeric_limits<uint64_t>::max(), // n.b. this is deliberately int64 max rather than uint64 max because
// this goes through the Nix language JSON parser and thus needs to be
// representable in Nix language integers.
std::numeric_limits<int64_t>::max(),
"warn-large-path-threshold", "warn-large-path-threshold",
R"( R"(
Warn when copying a path larger than this number of bytes to the Nix store Warn when copying a path larger than this number of bytes to the Nix store

View file

@ -94,7 +94,7 @@ static bool componentsLT(const std::string_view c1, const std::string_view c2)
} }
int compareVersions(const std::string_view v1, const std::string_view v2) std::strong_ordering compareVersions(const std::string_view v1, const std::string_view v2)
{ {
auto p1 = v1.begin(); auto p1 = v1.begin();
auto p2 = v2.begin(); auto p2 = v2.begin();
@ -102,11 +102,11 @@ int compareVersions(const std::string_view v1, const std::string_view v2)
while (p1 != v1.end() || p2 != v2.end()) { while (p1 != v1.end() || p2 != v2.end()) {
auto c1 = nextComponent(p1, v1.end()); auto c1 = nextComponent(p1, v1.end());
auto c2 = nextComponent(p2, v2.end()); auto c2 = nextComponent(p2, v2.end());
if (componentsLT(c1, c2)) return -1; if (componentsLT(c1, c2)) return std::strong_ordering::less;
else if (componentsLT(c2, c1)) return 1; else if (componentsLT(c2, c1)) return std::strong_ordering::greater;
} }
return 0; return std::strong_ordering::equal;
} }

View file

@ -30,7 +30,7 @@ typedef std::list<DrvName> DrvNames;
std::string_view nextComponent(std::string_view::const_iterator & p, std::string_view nextComponent(std::string_view::const_iterator & p,
const std::string_view::const_iterator end); const std::string_view::const_iterator end);
int compareVersions(const std::string_view v1, const std::string_view v2); std::strong_ordering compareVersions(const std::string_view v1, const std::string_view v2);
DrvNames drvNamesFromArgs(const Strings & opArgs); DrvNames drvNamesFromArgs(const Strings & opArgs);
} }

View file

@ -0,0 +1,182 @@
#pragma once
/**
* @file Checked arithmetic with classes that make it hard to accidentally make something an unchecked operation.
*/
#include <compare>
#include <concepts> // IWYU pragma: keep
#include <exception>
#include <ostream>
#include <limits>
#include <optional>
#include <type_traits>
namespace nix::checked {
class DivideByZero : std::exception
{};
/**
* Numeric value enforcing checked arithmetic. Performing mathematical operations on such values will return a Result
* type which needs to be checked.
*/
template<std::integral T>
struct Checked
{
using Inner = T;
// TODO: this must be a "trivial default constructor", which means it
// cannot set the value to NOT DO UB on uninit.
T value;
Checked() = default;
explicit Checked(T const value)
: value{value}
{
}
Checked(Checked<T> const & other) = default;
Checked(Checked<T> && other) = default;
Checked<T> & operator=(Checked<T> const & other) = default;
std::strong_ordering operator<=>(Checked<T> const & other) const = default;
std::strong_ordering operator<=>(T const & other) const
{
return value <=> other;
}
explicit operator T() const
{
return value;
}
enum class OverflowKind {
NoOverflow,
Overflow,
DivByZero,
};
class Result
{
T value;
OverflowKind overflowed_;
public:
Result(T value, bool overflowed)
: value{value}
, overflowed_{overflowed ? OverflowKind::Overflow : OverflowKind::NoOverflow}
{
}
Result(T value, OverflowKind overflowed)
: value{value}
, overflowed_{overflowed}
{
}
bool operator==(Result other) const
{
return value == other.value && overflowed_ == other.overflowed_;
}
std::optional<T> valueChecked() const
{
if (overflowed_ != OverflowKind::NoOverflow) {
return std::nullopt;
} else {
return value;
}
}
/**
* Returns the result as if the arithmetic were performed as wrapping arithmetic.
*
* \throws DivideByZero if the operation was a divide by zero.
*/
T valueWrapping() const
{
if (overflowed_ == OverflowKind::DivByZero) {
throw DivideByZero{};
}
return value;
}
bool overflowed() const
{
return overflowed_ == OverflowKind::Overflow;
}
bool divideByZero() const
{
return overflowed_ == OverflowKind::DivByZero;
}
};
Result operator+(Checked<T> const other) const
{
return (*this) + other.value;
}
Result operator+(T const other) const
{
T result;
bool overflowed = __builtin_add_overflow(value, other, &result);
return Result{result, overflowed};
}
Result operator-(Checked<T> const other) const
{
return (*this) - other.value;
}
Result operator-(T const other) const
{
T result;
bool overflowed = __builtin_sub_overflow(value, other, &result);
return Result{result, overflowed};
}
Result operator*(Checked<T> const other) const
{
return (*this) * other.value;
}
Result operator*(T const other) const
{
T result;
bool overflowed = __builtin_mul_overflow(value, other, &result);
return Result{result, overflowed};
}
Result operator/(Checked<T> const other) const
{
return (*this) / other.value;
}
/**
* Performs a checked division.
*
* If the right hand side is zero, the result is marked as a DivByZero and
* valueWrapping will throw.
*/
Result operator/(T const other) const
{
constexpr T const minV = std::numeric_limits<T>::min();
// It's only possible to overflow with signed division since doing so
// requires crossing the two's complement limits by MIN / -1 (since
// two's complement has one more in range in the negative direction
// than in the positive one).
if (std::is_signed<T>() && (value == minV && other == -1)) {
return Result{minV, true};
} else if (other == 0) {
return Result{0, OverflowKind::DivByZero};
} else {
T result = value / other;
return Result{result, false};
}
}
};
template<std::integral T>
std::ostream & operator<<(std::ostream & ios, Checked<T> v)
{
ios << v.value;
return ios;
}
}

View file

@ -172,6 +172,7 @@ headers = [config_h] + files(
'args/root.hh', 'args/root.hh',
'callback.hh', 'callback.hh',
'canon-path.hh', 'canon-path.hh',
'checked-arithmetic.hh',
'chunked-vector.hh', 'chunked-vector.hh',
'closure.hh', 'closure.hh',
'comparator.hh', 'comparator.hh',

View file

@ -204,15 +204,15 @@ static void loadDerivations(EvalState & state, const SourcePath & nixExprPath,
} }
static long getPriority(EvalState & state, PackageInfo & drv) static NixInt getPriority(EvalState & state, PackageInfo & drv)
{ {
return drv.queryMetaInt("priority", 0); return drv.queryMetaInt("priority", NixInt(0));
} }
static long comparePriorities(EvalState & state, PackageInfo & drv1, PackageInfo & drv2) static std::strong_ordering comparePriorities(EvalState & state, PackageInfo & drv1, PackageInfo & drv2)
{ {
return getPriority(state, drv2) - getPriority(state, drv1); return getPriority(state, drv2) <=> getPriority(state, drv1);
} }
@ -280,7 +280,7 @@ std::vector<Match> pickNewestOnly(EvalState & state, std::vector<Match> matches)
auto & oneDrv = match.packageInfo; auto & oneDrv = match.packageInfo;
const auto drvName = DrvName { oneDrv.queryName() }; const auto drvName = DrvName { oneDrv.queryName() };
long comparison = 1; std::strong_ordering comparison = std::strong_ordering::greater;
const auto itOther = newest.find(drvName.name); const auto itOther = newest.find(drvName.name);
@ -288,9 +288,9 @@ std::vector<Match> pickNewestOnly(EvalState & state, std::vector<Match> matches)
auto & newestDrv = itOther->second.packageInfo; auto & newestDrv = itOther->second.packageInfo;
comparison = comparison =
oneDrv.querySystem() == newestDrv.querySystem() ? 0 : oneDrv.querySystem() == newestDrv.querySystem() ? std::strong_ordering::equal :
oneDrv.querySystem() == settings.thisSystem ? 1 : oneDrv.querySystem() == settings.thisSystem ? std::strong_ordering::greater :
newestDrv.querySystem() == settings.thisSystem ? -1 : 0; newestDrv.querySystem() == settings.thisSystem ? std::strong_ordering::less : std::strong_ordering::equal;
if (comparison == 0) if (comparison == 0)
comparison = comparePriorities(state, oneDrv, newestDrv); comparison = comparePriorities(state, oneDrv, newestDrv);
if (comparison == 0) if (comparison == 0)
@ -625,13 +625,13 @@ static void upgradeDerivations(Globals & globals,
continue; continue;
DrvName newName(j->queryName()); DrvName newName(j->queryName());
if (newName.name == drvName.name) { if (newName.name == drvName.name) {
int d = compareVersions(drvName.version, newName.version); std::strong_ordering d = compareVersions(drvName.version, newName.version);
if ((upgradeType == utLt && d < 0) || if ((upgradeType == utLt && d < 0) ||
(upgradeType == utLeq && d <= 0) || (upgradeType == utLeq && d <= 0) ||
(upgradeType == utEq && d == 0) || (upgradeType == utEq && d == 0) ||
upgradeType == utAlways) upgradeType == utAlways)
{ {
long d2 = -1; std::strong_ordering d2 = std::strong_ordering::less;
if (bestElem != availElems.end()) { if (bestElem != availElems.end()) {
d2 = comparePriorities(*globals.state, *bestElem, *j); d2 = comparePriorities(*globals.state, *bestElem, *j);
if (d2 == 0) d2 = compareVersions(bestVersion, newName.version); if (d2 == 0) d2 = compareVersions(bestVersion, newName.version);
@ -902,7 +902,7 @@ static VersionDiff compareVersionAgainstSet(
for (auto & i : elems) { for (auto & i : elems) {
DrvName name2(i.queryName()); DrvName name2(i.queryName());
if (name.name == name2.name) { if (name.name == name2.name) {
int d = compareVersions(name.version, name2.version); std::strong_ordering d = compareVersions(name.version, name2.version);
if (d < 0) { if (d < 0) {
diff = cvGreater; diff = cvGreater;
version = name2.version; version = name2.version;

View file

@ -0,0 +1,8 @@
error:
… while calling the 'fetchTree' builtin
at /pwd/lang/eval-fail-fetchTree-negative.nix:1:1:
1| builtins.fetchTree {
| ^
2| type = "file";
error: negative value given for fetchTree attr owner: -1

View file

@ -0,0 +1,5 @@
builtins.fetchTree {
type = "file";
url = "file://eval-fail-fetchTree-negative.nix";
owner = -1;
}

View file

@ -0,0 +1,14 @@
error:
… while calling the 'seq' builtin
at /pwd/lang/eval-fail-flake-ref-to-string-negative-integer.nix:1:16:
1| let n = -1; in builtins.seq n (builtins.flakeRefToString {
| ^
2| type = "github";
… while calling the 'flakeRefToString' builtin
at /pwd/lang/eval-fail-flake-ref-to-string-negative-integer.nix:1:32:
1| let n = -1; in builtins.seq n (builtins.flakeRefToString {
| ^
2| type = "github";
error: negative value given for flake ref attr repo: -1

View file

@ -0,0 +1,7 @@
let n = -1; in builtins.seq n (builtins.flakeRefToString {
type = "github";
owner = "NixOS";
repo = n;
ref = "23.05";
dir = "lib";
})

View file

@ -0,0 +1,8 @@
error:
… while calling the 'fromJSON' builtin
at /pwd/lang/eval-fail-fromJSON-overflowing.nix:1:1:
1| builtins.fromJSON ''{"attr": 18446744073709551615}''
| ^
2|
error: unsigned json number 18446744073709551615 outside of Nix integer range

View file

@ -0,0 +1 @@
builtins.fromJSON ''{"attr": 18446744073709551615}''

View file

@ -0,0 +1,6 @@
error: integer overflow in adding 9223372036854775807 + 1
at /pwd/lang/eval-fail-overflowing-add.nix:4:8:
3| b = 1;
4| in a + b
| ^
5|

View file

@ -0,0 +1,4 @@
let
a = 9223372036854775807;
b = 1;
in a + b

View file

@ -0,0 +1,23 @@
error:
… while calling the 'seq' builtin
at /pwd/lang/eval-fail-overflowing-div.nix:7:4:
6| b = -1;
7| in builtins.seq intMin (builtins.seq b (intMin / b))
| ^
8|
… while calling the 'seq' builtin
at /pwd/lang/eval-fail-overflowing-div.nix:7:25:
6| b = -1;
7| in builtins.seq intMin (builtins.seq b (intMin / b))
| ^
8|
… while calling the 'div' builtin
at /pwd/lang/eval-fail-overflowing-div.nix:7:48:
6| b = -1;
7| in builtins.seq intMin (builtins.seq b (intMin / b))
| ^
8|
error: integer overflow in dividing -9223372036854775808 / -1

View file

@ -0,0 +1,7 @@
let
# lol, this has to be written as an expression like this because negative
# numbers use unary negation rather than parsing directly, and 2**63 is out
# of range
intMin = -9223372036854775807 - 1;
b = -1;
in builtins.seq intMin (builtins.seq b (intMin / b))

View file

@ -0,0 +1,16 @@
error:
… while calling the 'mul' builtin
at /pwd/lang/eval-fail-overflowing-mul.nix:3:10:
2| a = 4294967297;
3| in a * a * a
| ^
4|
… while calling the 'mul' builtin
at /pwd/lang/eval-fail-overflowing-mul.nix:3:6:
2| a = 4294967297;
3| in a * a * a
| ^
4|
error: integer overflow in multiplying 4294967297 * 4294967297

View file

@ -0,0 +1,3 @@
let
a = 4294967297;
in a * a * a

View file

@ -0,0 +1,9 @@
error:
… while calling the 'sub' builtin
at /pwd/lang/eval-fail-overflowing-sub.nix:4:6:
3| b = 2;
4| in a - b
| ^
5|
error: integer overflow in subtracting -9223372036854775807 - 2

View file

@ -0,0 +1,4 @@
let
a = -9223372036854775807;
b = 2;
in a - b

View file

@ -85,7 +85,7 @@ namespace nix {
if (arg.type() != nInt) { if (arg.type() != nInt) {
return false; return false;
} }
return arg.integer() == v; return arg.integer().value == v;
} }
MATCHER_P(IsFloatEq, v, fmt("The float is equal to \"%1%\"", v)) { MATCHER_P(IsFloatEq, v, fmt("The float is equal to \"%1%\"", v)) {

View file

@ -0,0 +1,54 @@
#pragma once
// SPDX-FileCopyrightText: 2014 Emil Eriksson
//
// SPDX-License-Identifier: BSD-2-Clause
//
// The lion's share of this code is copy pasted directly out of RapidCheck
// headers, so the copyright is set accordingly.
/**
* @file Implements the ability to run a RapidCheck test under gtest with changed
* test parameters such as the number of tests to run. This is useful for
* running very large numbers of the extremely cheap property tests.
*/
#include <gtest/gtest.h>
#include <rapidcheck/gtest.h>
#include <rapidcheck/gen/Arbitrary.hpp>
namespace rc::detail {
using MakeTestParams = TestParams (*)();
template<typename Testable>
void checkGTestWith(Testable && testable, MakeTestParams makeTestParams)
{
const auto testInfo = ::testing::UnitTest::GetInstance()->current_test_info();
detail::TestMetadata metadata;
metadata.id = std::string(testInfo->test_case_name()) + "/" + std::string(testInfo->name());
metadata.description = std::string(testInfo->name());
const auto result = checkTestable(std::forward<Testable>(testable), metadata, makeTestParams());
if (result.template is<detail::SuccessResult>()) {
const auto success = result.template get<detail::SuccessResult>();
if (!success.distribution.empty()) {
printResultMessage(result, std::cout);
std::cout << std::endl;
}
} else {
std::ostringstream ss;
printResultMessage(result, ss);
FAIL() << ss.str() << std::endl;
}
}
}
#define RC_GTEST_PROP_WITH_PARAMS(TestCase, Name, MakeParams, ArgList) \
void rapidCheck_propImpl_##TestCase##_##Name ArgList; \
\
TEST(TestCase, Name) \
{ \
::rc::detail::checkGTestWith(&rapidCheck_propImpl_##TestCase##_##Name, MakeParams); \
} \
\
void rapidCheck_propImpl_##TestCase##_##Name ArgList

View file

@ -0,0 +1,158 @@
#include <cstdint>
#include <gtest/gtest.h>
#include <limits>
#include <rapidcheck/Assertions.h>
#include <rapidcheck/gtest.h>
#include <rapidcheck/gen/Arbitrary.hpp>
#include <checked-arithmetic.hh>
#include "tests/gtest-with-params.hh"
namespace rc {
using namespace nix;
template<std::integral T>
struct Arbitrary<nix::checked::Checked<T>>
{
static Gen<nix::checked::Checked<T>> arbitrary()
{
return gen::arbitrary<T>();
}
};
}
namespace nix::checked {
// Pointer to member function! Mildly gross.
template<std::integral T>
using Oper = Checked<T>::Result (Checked<T>::*)(T const other) const;
template<std::integral T>
using ReferenceOper = T (*)(T a, T b);
/**
* Checks that performing an operation that overflows into an inaccurate result
* has the desired behaviour.
*
* TBig is a type large enough to represent all results of TSmall operations.
*/
template<std::integral TSmall, std::integral TBig>
void checkType(TSmall a_, TSmall b, Oper<TSmall> oper, ReferenceOper<TBig> reference)
{
// Sufficient to fit all values
TBig referenceResult = reference(a_, b);
constexpr const TSmall minV = std::numeric_limits<TSmall>::min();
constexpr const TSmall maxV = std::numeric_limits<TSmall>::max();
Checked<TSmall> a{a_};
auto result = (a.*(oper))(b);
// Just truncate it to get the in-range result
RC_ASSERT(result.valueWrapping() == static_cast<TSmall>(referenceResult));
if (referenceResult > maxV || referenceResult < minV) {
RC_ASSERT(result.overflowed());
RC_ASSERT(!result.valueChecked().has_value());
} else {
RC_ASSERT(!result.overflowed());
RC_ASSERT(result.valueChecked().has_value());
RC_ASSERT(*result.valueChecked() == referenceResult);
}
}
/**
* Checks that performing an operation that overflows into an inaccurate result
* has the desired behaviour.
*
* TBig is a type large enough to represent all results of TSmall operations.
*/
template<std::integral TSmall, std::integral TBig>
void checkDivision(TSmall a_, TSmall b)
{
// Sufficient to fit all values
constexpr const TSmall minV = std::numeric_limits<TSmall>::min();
Checked<TSmall> a{a_};
auto result = a / b;
if (std::is_signed<TSmall>() && a_ == minV && b == -1) {
// This is the only possible overflow condition
RC_ASSERT(result.valueWrapping() == minV);
RC_ASSERT(result.overflowed());
} else if (b == 0) {
RC_ASSERT(result.divideByZero());
RC_ASSERT_THROWS_AS(result.valueWrapping(), nix::checked::DivideByZero);
RC_ASSERT(result.valueChecked() == std::nullopt);
} else {
TBig referenceResult = a_ / b;
auto result_ = result.valueChecked();
RC_ASSERT(result_.has_value());
RC_ASSERT(*result_ == referenceResult);
RC_ASSERT(result.valueWrapping() == referenceResult);
}
}
/** Creates parameters that perform a more adequate number of checks to validate
* extremely cheap tests such as arithmetic tests */
static rc::detail::TestParams makeParams()
{
auto const & conf = rc::detail::configuration();
auto newParams = conf.testParams;
newParams.maxSuccess = 10000;
return newParams;
}
RC_GTEST_PROP_WITH_PARAMS(Checked, add_unsigned, makeParams, (uint16_t a, uint16_t b))
{
checkType<uint16_t, int32_t>(a, b, &Checked<uint16_t>::operator+, [](int32_t a, int32_t b) { return a + b; });
}
RC_GTEST_PROP_WITH_PARAMS(Checked, add_signed, makeParams, (int16_t a, int16_t b))
{
checkType<int16_t, int32_t>(a, b, &Checked<int16_t>::operator+, [](int32_t a, int32_t b) { return a + b; });
}
RC_GTEST_PROP_WITH_PARAMS(Checked, sub_unsigned, makeParams, (uint16_t a, uint16_t b))
{
checkType<uint16_t, int32_t>(a, b, &Checked<uint16_t>::operator-, [](int32_t a, int32_t b) { return a - b; });
}
RC_GTEST_PROP_WITH_PARAMS(Checked, sub_signed, makeParams, (int16_t a, int16_t b))
{
checkType<int16_t, int32_t>(a, b, &Checked<int16_t>::operator-, [](int32_t a, int32_t b) { return a - b; });
}
RC_GTEST_PROP_WITH_PARAMS(Checked, mul_unsigned, makeParams, (uint16_t a, uint16_t b))
{
checkType<uint16_t, int64_t>(a, b, &Checked<uint16_t>::operator*, [](int64_t a, int64_t b) { return a * b; });
}
RC_GTEST_PROP_WITH_PARAMS(Checked, mul_signed, makeParams, (int16_t a, int16_t b))
{
checkType<int16_t, int64_t>(a, b, &Checked<int16_t>::operator*, [](int64_t a, int64_t b) { return a * b; });
}
RC_GTEST_PROP_WITH_PARAMS(Checked, div_unsigned, makeParams, (uint16_t a, uint16_t b))
{
checkDivision<uint16_t, int64_t>(a, b);
}
RC_GTEST_PROP_WITH_PARAMS(Checked, div_signed, makeParams, (int16_t a, int16_t b))
{
checkDivision<int16_t, int64_t>(a, b);
}
// Make absolutely sure that we check the special cases if the proptest
// generator does not come up with them. This one is especially important
// because it has very specific pairs required for the edge cases unlike the
// others.
TEST(Checked, div_signed_special_cases)
{
checkDivision<int16_t, int64_t>(std::numeric_limits<int16_t>::min(), -1);
checkDivision<int16_t, int64_t>(std::numeric_limits<int16_t>::min(), 0);
checkDivision<int16_t, int64_t>(0, 0);
}
}