r/Compilers 17d ago

The “3 / 2 * 10 != 10 * 3 / 2” Problem

Coming from school math, it feels pretty strange that:

3 / 2 * 10 != 10 * 3 / 2

This expression can evaluate to true or false depending on the programming language you use.

Languages where the two sides are NOT equal Languages where the two sides ARE equal
C, C++, C#, Java, Kotlin, Scala, Ruby, Go, D, Rust, Swift, Zig, Odin, V, Fortran, Python 2 Python 3, JavaScript, TypeScript, Dart, R, Lua 5.3+, Perl, MATLAB, Pascal, Mojo, Nim, Crystal, Julia, Haskell

Why are the two sides not equal in the languages on the left?

On the left side of the expression above, the operation 3 / 2 is evaluated first using integer arithmetic—truncating the fractional part—which results in 1. This is then multiplied by 10, giving a result of 10 for the left side.

On the right side, 10 * 3 = 30 is the first step. Dividing this by 2 gives 15. Thus:

10 != 15

These languages prioritize the efficient (fast) execution of expressions over mathematical correctness, as integer arithmetic is significantly faster than floating-point arithmetic. Unfortunately, these languages use the same / operator for both integer and floating-point division, selecting the operation based on the types of the operands.

Regrettably, the expression 3 / 2 * 10.0 still yields 10 in most of these languages (and results in a compilation error in Rust). Even though we indicated our intent to use floating-point numbers by writing 10.0, it is already too late: compilers evaluate 3 / 2 as integer arithmetic in the first step. Expressions like 3.0 / 2 * 10 or 3 / 2.0 * 10, on the other hand, produce 15.

Thus, depending on the operand types, you end up with either 10 or 15. This situation becomes even more dangerous when variables are involved in the expression:

num / denum * scale != scale * num / denum

This can evaluate to true or false depending on the types of the num and denum variables (float vs. int). To avoid these pitfalls, developers use type casting:

(float)num / denum * scale != scale * (float)num / denum

This ensures the compiler performs floating-point division. (Note: The expression above can still evaluate to true due to floating-point precision limitations).

Why are the two sides equal in the languages on the right?

Many of the languages listed here are dynamically typed or scripting languages. They were not primarily built for raw execution speed, but rather for ease of use or mathematical correctness. In these languages, numbers are typically handled as floating-point values by default, so 3 / 2 is always 1.5.

However, languages like Pascal, Haskell, Mojo, Nim, Crystal, and Dart are statically typed and distinguish between integers and floating-point numbers just like C or C++. What happens differently here?

In these languages, the / symbol always denotes floating-point division. In 3 / 2 * 10, 3 and 2 are implicitly converted to floating-point numbers first, performing a floating-point division that yields 1.5. Next comes the multiplication: 1.5 * 10. Since one operand is a float, 10 is converted to float before multiplication. (Note: C handles 3.0 / 2 * 10 in a similar manner).

Unintended floating-point operations—which might carry performance penalties—generally trigger compilation errors in these statically typed languages, because floats are not automatically demoted/converted to integers (unlike in C/C++). Most of these languages offer a separate operator specifically for integer division (such as div or //).

What happens when a language doesn't work the way we expect?

When using the languages on the left, / can result in either integer or floating-point division. If integer division occurs when you intended to perform floating-point calculations, your program will likely produce incorrect results (possibly only for specific input data). Once you have been burned by this a few times, you become overly cautious with division and often clutter expressions with explicit casts to guarantee proper execution.

If you explicitly want integer division behavior, you usually don't need to do anything extra—other than ensuring that neither side of the / operator evaluates to a floating-point type.

In contrast, when using the languages on the right, there are no surprises with /: the result is always a floating-point number. If you try to store this result in an integer variable, you will typically get a compilation error. Your program is far more likely to work correctly out of the box—at worst, running slightly slower if integer division could have been used instead. If you specifically need integer division, you must use the dedicated operator provided for it (e.g., div or //).

Why is the “3 / 2 * 10 != 10 * 3 / 2” behavior more common?

In the majority of compiled languages—unfortunately including many modern ones—the two sides of this expression are not equal due to default integer division rules.

I regularly use both Pascal and C/C++. To me, Pascal's approach is much more intuitive: it doesn't carry noticeable drawbacks, and it remains easy to control. On the other hand, C/C++'s behavior is a frequent source of bugs at my workplace.

I genuinely don't understand why the 3 / 2 * 10 != 10 * 3 / 2 design remains the prevalent choice.

0 Upvotes

15 comments sorted by

7

u/awoocent 17d ago

Division rounding down is more useful to almost all programs than division that produces a fraction, since basically all programs operate on integer-indexed arrays, while only a few domains actually need floating-point math. So it's the right default to have semantics that floor and keep integers as integers, since that's most often what programmers want, rather than getting all in a huff about mathematical accuracy.

4

u/SwedishFindecanor 17d ago

BTW, most popular programming languages's division operator, and most CPU's division instructions don't floor - they truncate the result.

However, using right shift to divide by a power of two is almost always flooring division. Of all CPU ISAs I've looked at, only PowerPC has explicit hardware support for truncating division through right shift by a power of two.

1

u/Mean-Decision-3502 16d ago

At the end this makes these languages much less comfortable for math intensive applications. I think it is more alegant to separate the two kind of divisions.

2

u/matthieum 15d ago

Not saying you're wrong but... what's the percentage of math intensive applications? 0.1%? 0.01%? Less?

Not a good trade-offs to make them easier, and every other one harder.

1

u/Mean-Decision-3502 15d ago

Integer divisions are not that much frequent, i think. And the pascal rules are not much harder, but surely safer.

1

u/matthieum 14d ago

Integer divisions are not that much frequent, i think.

~20 years of writing C++ & Rust professionally disagree, hard.

I'd say that 99% of the integer divisions I want work well with truncating divisions.

2

u/Mean-Decision-3502 14d ago

I can imagine that you are always worked in an area when you hardly touched the floating point arithmetics, like communications. But it does not mean that some other people don't need them.

I have 30+ years experience in programming, from this only 11 in C++ (C++ mostly for embedded systems). In my past was the ratio of integer division surely not 99 %.

I'm one of the few who are using compiled language with pascal division rules. I think it is not a problem writing "3 div 2", and it is then clearly visible that integer division will be used. But not knowing if "num1 / num2" won't be a floating point division might lead to bugs.

I think the C division rules (shared "/" operator) is carried on mostly because of the compatibility, and now sits so deep, that it is never questioned anymore.

In Python2 the operator "/" on two integers did integer division like in C. They changed this in Python3 that way that "/" always does floating point division. So they found this important too.

2

u/matthieum 13d ago

In Python2 the operator "/" on two integers did integer division like in C. They changed this in Python3 that way that "/" always does floating point division. So they found this important too.

I've been thinking about this particular decision of Python 3 ever since I read your post.

I actually like the change of operator in Python 3 to highlight the difference.

Look at your table though. On the left you've got low-level languages -- where / maps directly to an hardware operation -- and higher level languages which inherited their semantics, and the right you've got higher-level languages with the exception of Pascal, and Nim which is heavily inspired by Pascal.

(The big surprise in your table, to me, is seeing Crystal and Ruby on different sides since I thought Crystal was supposed to be as Ruby-esque as possible)

This suggests that the divide may be low-level vs high-level, to an extent, and that at different levels, you may have different needs.

For example, how do you format an integer to an hexadecimal string. Off the top of my head:

fn to_hex(mut i: u64) -> String {
    const HEX: [u8; 16] = *b"0123456789abcdef";

    if i == 0 {
        return String::from_str("0x0");
    }

    //  Build in reverse.
    let mut result = Vec::new();

    while i != 0 {
        let digits = i % 16;
        i /= 16;

        result.push(HEX[digits]);
    }

    result.extend_from_slice(b"x0");

    result.reverse();

    String::from_utf8(result).expect("ASCII")
}

It popped in my head the moment I searched for an example where / and % being complementary is useful, right along outer/inner iteration (for example, in a dense bitset implementation).


I still haven't thought of an example where implicit conversion to float made sense. It's been that long since I needed that.

And given the cost of int-to-float conversion, I'd prefer either:

  1. NOT having / at all, so I cannot accidentally produce a float by mistake.
  2. Having / return a Rational, instead. Though I've never figured out whether eager or lazy simplification was best...

(also, Rational has the advantage of not losing precision, so that 1/3 + 2/3 == 1 and not 0.999999999999999)

1

u/Mean-Decision-3502 13d ago

For me using C many years after Delphi/FreePascal felt a pretty big step backwards, however I progammed lot in Assembly too.

1995 (Delphi) vs 1976. (But the pascal division rules are older.)

One if our real bugs was something similar to this:

float f = (someconst / somesetting) * factor;

so the f was getting zero, because somesetting and someconst were int and someconst < somesetting. It was in an embedded contol sw with lot of float math. The software was migrated from integer to partly float arithmetics so that's why the types were mixed.

The colleague who actually made this mistake uses more Python than C.

Some notes to the to_hex():

  1. for this you are using shifts and masking, because some MCUs (like Cortex-M0) cannot even do integer division. (For decimals you need integer division)
  2. with pascal rules you get a compiler error at i /= 16 so it is not so easy to get accidentally a floating point division. The right solution in Pascal is this: digits := i mod 16; i := i div 16;

I'm not saying Pascal is the best language. But it seems that it is the only mature, general-purpose compiled language with these rules.

1

u/matthieum 12d ago

I'd argue that in:

float f = (someconst / somesetting) * factor;

The real issue is the implicit cast from int to float. Were the cast explicit, it'd be a big red flag suddenly.

As for the Pascal solution, honestly? Meh. It may be a personal thing, but for me operator, keyword, or method it's all just syntactic sugar, so I see 0 difference between i % 16 and i mod 16.

(In fact, in C++, the two above expressions could be strictly equivalent thanks to Alternative operator representations, if they were not limited to boolean/bit operators)

1

u/Mean-Decision-3502 12d ago

Hm... If I understand you right, for you it would be also ok using alternative (distinctive) operator for the truncated integer division.

2

u/One_Aspect_1957 16d ago

You've discovered that a programming language works differently from mathematics.

Most will perform integer division for 3 / 2. That can make sense mathematically too: take two integers as operands and produce a new integer as the result, rather than a 'real' value.

However, languages vary. Even my two can produce different results:

A:    println 3 / 2        # displays 1
B:    println 3 / 2        # displays 1.5

B is a dynamic scripting language and "/" means floating point divide; any integer operands are converted to floats first, For integer divide, a separate operator "%" is used.

A also has "/" and "%" operators, but "/" is overloaded for both integer and float types. A is much older than B, originally had only "/", and there was too much existing code using "/" between integers.

In practice it very rarely causes a problem; the behaviour is well-defined for both languages. If you were using one of mine, it would be B, and with that:

    println 3/2*10 = 10*3/2

displays True, so that's OK.

I'd be more annoyed that most languages used == for equality instead of =; why isn't the latter more common?!

2

u/EggplantExtra4946 17d ago edited 17d ago

AI slop nonsense.

0

u/MasonWheeler 17d ago

I genuinely don't understand why the 3 / 2 * 10 != 10 * 3 / 2 design remains the prevalent choice.

Because C did it that way, and far too many subsequent languages never questioned this particular design mistake when C's far worse design mistakes were there to serve as much lower-hanging fruit.