> Yeah, this is the devastating observation that undermines the whole clean taxonomy. It goes even deeper than people usually acknowledge.
Just integers alone: C says int is "at least 16 bits," could be 32 or 64. Java says int is always 32-bit signed twos-complement. JavaScript has no integers — everything is f64, and |0 is a convention not a type. Python integers are arbitrary precision by default. Rust has i8 through i128 plus isize which is platform-dependent. Haskell's Int is "at least 30 bits" per the spec but GHC makes it machine-word-sized, while Integer is arbitrary precision.
And that's just width. Overflow behavior is where it gets genuinely treacherous. C says signed overflow is undefined behavior — the compiler can assume it never happens and optimize based on that assumption, which means your program can do literally anything. Rust panics on debug, wraps on release, unless you explicitly pick wrapping_add or checked_add or saturating_add. Java wraps silently. Python can't overflow because integers grow. JavaScript... you get floating point rounding once you exceed 2^53, silently, and then n + 1 === n evaluates to true.
Division semantics differ too. -7 / 2 is -3 in most languages (truncation toward zero) but -4 in Python (floor division). The modulo operator disagrees across languages in the same way. This isn't academic — it causes real bugs when porting numeric code.
And this is the thing everyone supposedly agrees on
Integers are the canonical example of something "every language has." If we can't even agree on what a + b means, the whole portability spectrum I described is built on sand. What I called "Level 0 — universal" doesn't actually exist. Even fibonacci(n) isn't portable because for large enough n, it overflows differently per target, or doesn't overflow at all.
This extends to basically every "primitive":
Floating point is the closest to standardized thanks to IEEE 754, but languages differ on whether they expose NaN-boxing, how they handle denormals, whether -0.0 == 0.0, and whether the optimizer is allowed to reassociate operations (C with -ffast-math says yes, which breaks IEEE semantics entirely).
Strings are a nightmare. Rust has UTF-8 byte sequences. Java has UTF-16 code units. Python 3 has an abstract unicode sequence with an internal representation that varies. JavaScript uses UTF-16 but exposes individual code units, so "😀".length === 2. Go strings are UTF-8 byte slices but indexing gives you bytes not characters. Even "the length of a string" is not a well-defined concept across languages.
Booleans seem safe until you realize C didn't even have a boolean type until C99, _Bool can be 0 or 1 but any nonzero integer is truthy, while in Python bool is a subclass of int so True + True === 2, and in JavaScript [] == false but if ([]) takes the truthy branch.
Equality itself is incoherent across targets. Reference equality vs structural equality vs some hybrid. JavaScript has == vs ===. Java's == on objects is reference comparison, .equals() is structural but only if the class implements it. Haskell's ==is typeclass-dispatched. Python's is vs ==. Floating point NaN isn't equal to itself in IEEE 754, but some languages have container equality that disagrees with element equality.
What this really tells us
The honest conclusion is that there is no portable semantic foundation, not even at the level of arithmetic. Every "cross-platform" system quietly picks a semantics and then has impedance mismatches with every target. The question isn't whether to have mismatches but which mismatches you're willing to tolerate.
The approaches that actually work in practice tend to pick one of these strategies:
Lowest common denominator. Define integers as arbitrary precision, strings as unicode scalar sequences, floating point as strict IEEE 754. This is semantically clean and maximally portable, but it means emitting arbitrary-precision arithmetic on targets that don't need it — your "portable" fibonacci now runs 100x slower on C than native C because every addition goes through a bignum library when a machine instruction would have sufficed. This is roughly what Erlang/Elixir do and the performance characteristics are accepted as a tradeoff of the platform.
Pick a concrete semantics and document the gaps. This is what Java did (everything is specified precisely — 32-bit signed twos-complement wrapping integers, IEEE 754 doubles, UTF-16 strings) and it works great as long as your only target is the JVM. The moment you try to cross platforms, those precise choices become the source of mismatches.
Make the programmer choose explicitly. Rust's approach of having wrapping_add, checked_add, saturating_add, overflowing_add as separate operations. This acknowledges that "addition" is not one operation but a family of operations with different edge-case behaviors. A cross-platform kernel could adopt this — make the programmer state their intent about overflow, and then each target backend maps to the appropriate implementation. But now you've made the programmer think about something they usually don't want to think about, and you've lost the simplicity that makes a language adoptable.
Give up on semantic portability and only guarantee behavioral equivalence for some well-defined subset of inputs. This is what most practical cross-platform tools actually do. Your code is portable as long as your integers stay under 2^31, your strings are ASCII, you don't depend on hash ordering, and you don't observe overflow. The moment you leave that safe zone, behavior diverges. Nobody says this out loud but it's the pragmatic reality.
The philosophical upshot
I think what your observation really points to is that "portability" was always a spectrum of probability of correct behavior across targets, not a binary property. Even the most "universal" operation — integer addition — has target-dependent semantics at the edges. The useful question isn't "is this portable" but "for what range of inputs and usage patterns will this behave identically across targets, and how likely is my program to stay within that range?"
Which is a much harder thing to analyze or communicate to a developer, and maybe explains why nobody has really built the tooling we were discussing. The honest version of the portability annotation would be something like:
a + b
// ⚠ Portable for |result| < 2^31
// Diverges on overflow:
// C: undefined behavior
// Java/Rust-release: wraps to negative
// Python: continues correctly (arbitrary precision)
// JS: loses precision above 2^53
And if even a + b needs a footnote that long, the dream of automated portability analysis starts to look less like an engineering problem and more like a fundamental tension in computing that we just manage rather than solve.