Earlier, we did reviews of the code of large mathematical packages, for example, Scilab and Octave, and calculators remained aside as small utilities that make it difficult to make mistakes due to their small amount of code. We made a mistake by not paying attention to them. The case of the publication of the source code of the Windows calculator showed that it is interesting for everyone to discuss what errors are hidden there, and there are more than enough errors to write about this article. My colleagues and I decided to investigate the code of a number of popular calculators and it turned out that the Windows calculator code was not so bad (spoiler).
Introduction
Qalculate! - universal cross-platform calculator. It is easy to use, but it provides the power and versatility typically found in complex math packages, as well as useful tools for everyday needs (such as currency conversion and interest calculation). The project consists of two components:
libqalculate (library and CLI) and
qalculate-gtk (GTK + UI). Only libqalculate code is involved in the study.
To make it easier to compare the project with the same Windows calculator that we recently researched, I’ll quote the output of the Cloc utility for libqalculate:
Subjectively, there are more errors and they are more critical than in the Windows calculator code. But I recommend to draw conclusions independently, having familiarized with the given review of the code.
')
By the way, here is the link to the article about checking the calculator from Microsoft: "
Let's calculate the bugs in the Windows calculator ".
PVS-Studio was used as a static analysis tool. This is a set of solutions for code quality control, searching for errors and potential vulnerabilities. Supported languages ​​include: C, C ++, C # and Java. Analyzer launch is possible on Windows, Linux and macOS.
Copy-paste and typos again!
V523 The 'then' statement is equivalent to the 'else' statement. Number.cc 4018
bool Number::square() { .... if(mpfr_cmpabs(i_value->internalLowerFloat(), i_value->internalUpperFloat()) > 0) { mpfr_sqr(f_tmp, i_value->internalLowerFloat(), MPFR_RNDU); mpfr_sub(f_rl, f_rl, f_tmp, MPFR_RNDD); } else { mpfr_sqr(f_tmp, i_value->internalLowerFloat(), MPFR_RNDU); mpfr_sub(f_rl, f_rl, f_tmp, MPFR_RNDD); } .... }
The code in the
if and
else statements is exactly the same. Neighboring code snippets are very similar to this, but they use different functions:
internalLowerFloat () and
internalUpperFloat () . It is safe to assume that here the programmer copied the code and forgot to correct the function name.
V501 There are identical sub-expressions '! Mtr2.number (). IsReal ()' operator. BuiltinFunctions.cc 6274
int IntegrateFunction::calculate(....) { .... if(!mtr2.isNumber() || !mtr2.number().isReal() || !mtr.isNumber() || !mtr2.number().isReal()) b_unknown_precision = true; .... }
Here, duplicate expressions arose from the fact that in one place instead of the name
mtr they wrote
mtr2 . Thus, in the condition there is no function call
mtr.number (). IsReal () .
V501 There are identical sub-expressions 'vargs [1] .representsNonPositive ()' operator. BuiltinFunctions.cc 5785

Finding anomalies in this code manually is unreal! But they are. And in the original file, these fragments are written in one line. The analyzer detected a duplicate expression
vargs [1] .representsNonPositive () , which may indicate a typo and, therefore, a potential error.
Here is a list of suspicious places that can hardly be sorted out:
- V501 There are identical sub-expressions 'vargs [1] .representsNonPositive ()' operator. BuiltinFunctions.cc 5788
- V501 There are identical sub-expressions of the operator. MathStructure.cc 1780
- V501 There are identical sub-expressions of the operator. MathStructure.cc 2043
- V501 There are identical sub-expressions '(* v_subs [v_order [1]]). RepresentsNegative (true)' the operator. MathStructure.cc 5569
Loop with invalid condition
V534 It is likely that a variable is being compared inside the 'for' operator. Consider reviewing 'i'. MathStructure.cc 28741
bool MathStructure::isolate_x_sub(....) { .... for(size_t i = 0; i < mvar->size(); i++) { if((*mvar)[i].contains(x_var)) { mvar2 = &(*mvar)[i]; if(mvar->isMultiplication()) { for(size_t i2 = 0; i < mvar2->size(); i2++) { if((*mvar2)[i2].contains(x_var)) {mvar2 = &(*mvar2)[i2]; break;} } } break; } } .... }
In the internal cycle, the counter is the variable
i2 , but due to a typo, an error is made - the condition for stopping the cycle is variable
i from the external cycle.
Redundancy or error?
V590 Consider inspecting this expression. The expression is misprint. Number.cc 6564
bool Number::add(const Number &o, MathOperation op) { .... if(i1 >= COMPARISON_RESULT_UNKNOWN && (i2 == COMPARISON_RESULT_UNKNOWN || i2 != COMPARISON_RESULT_LESS)) return false; .... }
Having seen enough of this code, 3 years ago I wrote a note to help myself and other programmers: "
Logical expressions in C / C ++. How professionals are mistaken ." Meeting this code, I am convinced that the note has not become any less relevant. You can look at the article, find the error pattern corresponding to the code, and find out all the nuances.
In the case of this example, go to the section "Expression == || ! = "And find out that the expression
i2 == COMPARISON_RESULT_UNKNOWN does not affect anything.
Dereferencing unverified pointers
V595 The 'o_data' pointer was used before it was verified against nullptr. Check lines: 1108, 1112. DataSet.cc 1108
string DataObjectArgument::subprintlong() const { string str = _("an object from"); str += " \""; str += o_data->title();
The
o_data pointer in one function is dereferenced without checking and with checking. This may be a redundant code or a potential error. I tend to the last option.
There are two more similar places:
- V595 The 'o_assumption' pointer was used before it was verified against nullptr. Check lines: 229, 230. Variable.cc 229
- V595 The 'i_value' pointer was used before it was verified against nullptr. Check lines: 3412, 3427. Number.cc 3412
free () or delete []?
V611 The memory was allocated using the 'new' operator. Consider inspecting operation logics behind the 'remcopy' variable. Number.cc 8123
string Number::print(....) const { .... while(!exact && precision2 > 0) { if(try_infinite_series) { remcopy = new mpz_t[1];
Memory for the
remcopy array
is allocated and released in various ways, which is a serious error.
Lost Changes
bool expand_partial_fractions(MathStructure &m, ....) { .... if(b_poly && !mquo.isZero()) { MathStructure m = mquo; if(!mrem.isZero()) { m += mrem; m.last() *= mtest[i]; m.childrenUpdated(); } expand_partial_fractions(m, eo, false); return true; } .... }
The variable
m is taken as a function by reference, which implies its modification. But the analyzer found that there is a local variable of the same name in the code that overlaps the scope of the parameter of the function, allowing for the loss of changes.
Strange signs
V774 The 'cu' pointer was used after the memory was released. Calculator.cc 3595
MathStructure Calculator::convertToBestUnit(....) { .... CompositeUnit *cu = new CompositeUnit("", "...."); cu->add(....); Unit *u = getBestUnit(cu, false, eo.local_currency_conversion); if(u == cu) { delete cu;
The analyzer warns that the code contains a call to the
cu object method after the memory is freed. But if you try to understand the code, it will be even more strange. First, the call
cu is always
invoked - in the condition and after. Secondly, the code after the condition assumes that the pointers
u and
cu are not equal, it means that after cleaning the
cu object, it is logical to use the object
u . Most likely, there was a typo in the code and it was planned to use only the variable
u .
Using the find function
V797 The 'find' function. The return value of the function should not be compared with std :: string :: npos. Unit.cc 404
MathStructure &AliasUnit::convertFromFirstBaseUnit(....) const { if(i_exp != 1) mexp /= i_exp; ParseOptions po; if(isApproximate() && suncertainty.empty() && precision() == -1) { if(sinverse.find(DOT) || svalue.find(DOT)) po.read_precision = READ_PRECISION_WHEN_DECIMALS; else po.read_precision = ALWAYS_READ_PRECISION; } .... }
Although the code successfully compiles, it looks suspicious, since the
find function returns a number of type
std :: string :: size_type . The condition will be true if the point is found anywhere on the line, unless the point is at the beginning. This is a strange test. I'm not sure, but maybe the code should be rewritten as follows:
if( sinverse.find(DOT) != std::string::npos || svalue.find(DOT) != std::string::npos) { po.read_precision = READ_PRECISION_WHEN_DECIMALS; }
Potential memory leak
V701 realloc () possible leak: when realloc () fails in allocating memory, the original pointer 'buffer' is lost. Consider assigning realloc () to a temporary pointer. util.cc 703
char *utf8_strdown(const char *str, int l) { #ifdef HAVE_ICU .... outlength = length + 4; buffer = (char*) realloc(buffer, outlength * sizeof(char));
When working with the
realloc () function, it is recommended to use an intermediate buffer, since if memory cannot be allocated, the pointer to the old memory location will be irretrievably lost.
Conclusion
Project Qalculate! tops the list of the best free calculators, while containing many serious mistakes. But we have not yet seen its competitors. We will try to go through all the popular calculators.
As for comparison with the quality of the calculator from the Windows world, while the utility from Microsoft looks more reliable and high-quality.
Check your “Calculator” by downloading
PVS-Studio and trying it on your project. :-)

If you want to share this article with an English-speaking audience, then please use the link to the translation: Svyatoslav Razmyslov.
Following in the Footsteps of Calculators: Qalculate!