Compatibility
Published
Arithmetic is the one area where a code transform genuinely could change results, because rewriting expressions means touching precedence and grouping. We measured the Math surface, floating point behaviour, the 32-bit bitwise operators and a real hash function across five configurations. Every result was identical.
Why arithmetic is a legitimate thing to check
Most compatibility worries about obfuscation are misplaced, because the transforms rename identifiers and restructure control flow without going near the semantics of an expression. Arithmetic is the exception worth testing, for one specific reason: protection rewrites expressions, and if grouping or precedence were altered anywhere, the symptom would be a number that is slightly wrong rather than a crash.
Slightly wrong numbers are the worst failure mode there is. They pass smoke tests, survive code review, and turn up in a reconciliation weeks later. So this area got the same treatment as the others: a sample, executed before and after, with output diffed line by line.
All five configurations matched: the ES5 target, the modern target, two identifier-renaming presets and the string-table preset.
The Math surface and floating point reality
The rounding family keeps its distinctions, including the ones that differ on negative numbers. Math.floor(-2.7) is -3 while Math.trunc(-2.9) is -2. Math.round(-2.5) is -2, which is the half-up-toward-positive-infinity rule people forget. Math.sign, Math.abs, Math.hypot, Math.log2, Math.log10, Math.clz32, Math.imul and Math.fround all returned their original values.
The empty-argument edge case holds too: Math.min() is Infinity and Math.max() is -Infinity. Code that reduces over a possibly-empty array relies on exactly that.
Floating point behaves like floating point, before and after. 0.1 + 0.2 is still 0.30000000000000004 and still not equal to 0.3, while the epsilon comparison still returns true. Addition is still non-associative, so grouping the same three values differently still gives 0.6000000000000001 one way and 0.6 the other. That last one is the sharpest evidence that expression rewriting is not silently regrouping arithmetic: if it were, that pair of values would have converged.
Infinities and negative zero are unchanged, including 1 / -0 giving -Infinity, and the loss of precision above the safe integer range is identical.
Bitwise operators stay 32-bit, and a real hash still matches
Bitwise operators in JavaScript coerce to 32-bit signed integers, which makes them the most fragile-looking arithmetic in the language and the most worth checking.
Every result was identical. The basic operators, the shifts, and the distinction between the signed and unsigned right shift all held, so -16 >> 2 is -4 while -16 >>> 2 is 1073741820 and -1 >>> 0 is 4294967295. Overflow wraps the same way: (2147483647 + 1) | 0 is -2147483648 and 4294967296 | 0 is 0. Even the three-step XOR swap returned its values swapped correctly.
The test that matters most is a real one rather than a synthetic operator table. The sample includes a hand-written FNV-1a hash built from charCodeAt, XOR, Math.imul and a final unsigned shift. Protected, it returns 1335831723 for the string hello, which is exactly what the unprotected version returns, and the empty-string case returns the FNV offset basis unchanged. If you compute checksums, cache keys or content hashes on the client, they still agree with a server that computes them the same way.
Modulo keeps the sign of the dividend, so -7 % 3 is -1 and 7 % -3 is 1, and the float case is unchanged.
Operator precedence survives the rewrite
This was included specifically to test the mechanism rather than the feature, because precedence is what an expression rewriter could plausibly get wrong.
Two expressions were written without clarifying parentheses. The first mixes addition, multiplication, exponentiation and division and evaluates to 8. The second relies on addition binding tighter than the shift operator, so 1 + 2 << 3 is 24 rather than 17. Both produced the same values after protection on every configuration.
That is a narrow test, but it is the right narrow test. If the engine were re-associating or re-parenthesising expressions incorrectly, an unparenthesised mixed-precedence expression is where it would show, and a shift mixed with addition is the classic case.
What to take from this
If your code does numeric work, protection is not a correctness risk. That covers pricing arithmetic, geometry, animation easing, checksums and client-side hashing. You do not need a numeric review as part of enabling protection.
Unlike the string and number-formatting areas, there is no member-renaming caveat here worth a warning, because Math methods are reached through the Math global rather than through your own objects. The same rule still applies in principle: a member pattern matching imul would rewrite Math.imul call sites and throw a TypeError on the first one, which we confirmed. Keep the pattern scoped to your own names and it cannot arise.
The one genuine caveat in numeric code has nothing to do with obfuscation: results that depend on Math.random are not reproducible, and results that depend on the device clock are not trustworthy. Those are covered in your random numbers run on their machine. Protection does not change either, and does not fix either.
Frequently asked questions
Does obfuscation change floating point results?
No. We diffed an arithmetic sample against its protected copy on five configurations and every value matched, including 0.1 + 0.2 producing 0.30000000000000004 and remaining unequal to 0.3. The non-associativity of addition also held, which is direct evidence that expression rewriting is not regrouping arithmetic.
Do bitwise operators still work correctly after protection?
Yes. The 32-bit semantics are fully preserved, including the difference between the signed and unsigned right shift, overflow wrapping at the 32-bit boundary, and negative operands. A three-step XOR swap also produced correctly swapped values.
Will a client-side hash or checksum still match the server?
Yes. The sample includes a hand-written FNV-1a hash using charCodeAt, XOR and Math.imul. The protected build returns 1335831723 for the string hello, identical to the unprotected build, and the empty-string case returns the offset basis unchanged. Content hashes and cache keys computed on the client still agree with a server computing them the same way.
Can expression rewriting change operator precedence?
It did not in our measurements. Two deliberately unparenthesised mixed-precedence expressions were included, one combining addition, multiplication, exponentiation and division, and one relying on addition binding tighter than the shift operator so that 1 + 2 << 3 is 24. Both produced identical values after protection on every configuration.
Are Math methods affected by identifier renaming?
No, because they are reached through the Math global rather than through your own objects. A member-renaming pattern broad enough to match a built-in name such as imul would rewrite those call sites and throw a TypeError on the first one, which we confirmed deliberately, but a pattern scoped to your own property names cannot cause it.
Does protection make my random numbers or timings different?
No, and it does not make them trustworthy either. Math.random still runs on the client and still produces different values per run, and anything depending on the device clock is still under the user's control. Protection does not change those properties, so any logic that needs unpredictable or authoritative values should compute them on your server.
Related reading