# 0090 — integer formatter can't render i64::MIN or unsigned all-ones > STATUS: RESOLVED (F0.8). Both extremes now render correctly: > `s64.min` → `-9223372036854775808`, `u64.max` → `18446744073709551615`. > > **Root cause.** > - Symptom 1 (i64::MIN): `std.int_to_string` computed the magnitude as > `0 - n`, which overflows for `s64::MIN` (its magnitude is > unrepresentable as a positive s64) — the value stayed negative, the > `while v > 0` loop ran zero times, and only the `-` was emitted. > - Symptom 2 (unsigned all-ones): `any_to_string`'s `case int:` arm > formatted every integer as s64 (`int_to_string(xx val)`); there was no > way to tell a `u64` from an `s64`, so an all-ones u64 printed as `-1`. > > **Fix per file.** > - `library/modules/std.sx` — `int_to_string` now extracts digits straight > from `n` (taking `|n % 10|` per digit, `n` truncates toward zero) so it > never negates `s64::MIN`. Added `uint_to_string` (unsigned decimal via > long-division-by-10 over four 16-bit limbs) and `decompose_u16x4` (the > shared 16-bit-limb split, now reused by `int_to_hex_string` too). > `any_to_string`'s `case int:` routes through the new > `type_is_unsigned(type)` query to pick the unsigned vs signed formatter. > Declared `type_is_unsigned :: ($T: Type) -> bool #builtin;`. > - `src/ir/types.zig` — `TypeTable.isUnsignedInt` (canonical signedness > predicate; single source of truth). > - `src/ir/inst.zig` — `type_is_unsigned` BuiltinId. > - `src/ir/calls.zig` — register `type_is_unsigned` as a `.bool` reflection > builtin. > - `src/ir/lower.zig` — `tryLowerReflectionCall` arm: static fold + > dynamic `callBuiltin`. > - `src/ir/interp.zig` — interp arm (reads the boxed TypeId / `type_of` > aggregate shape). > - `src/ir/emit_llvm.zig` + `src/backend/llvm/reflection.zig` + > `src/backend/llvm/ops.zig` — lazy `[N x i1]` `__sx_type_is_unsigned` > table built from `isUnsignedInt`; runtime arm GEPs in at the TypeId. > > **Regression test.** `examples/0046-basic-int-formatter-extremes.sx` > pins both extremes plus a width spread (s8/s16/s32 + u8/u16/u32/u64, > mins/maxes, 0, ordinary values). Unit tests: `isUnsignedInt` in > `src/ir/types.test.zig`. > STATUS (original): OPEN. Pre-existing + orthogonal; surfaced (not introduced) by NL.1. > Manager-verified independent of the numeric-limit accessors. Scheduled separately. ## Symptom `print("{}", x)` mis-renders the integer extremes the s64-based formatter can't represent: - `i64::MIN` (`-9223372036854775808`) prints a bare `-` (the minus sign with NO digits). - An unsigned all-ones value (e.g. `u64.max` = 18446744073709551615) prints `-1` (the i64 bit-reinterpretation), not the unsigned decimal. ## Reproduction (no numeric-limit accessor needed — pre-existing) ```sx #import "modules/std.sx"; main :: () { x := -9223372036854775807 - 1; // i64::MIN print("min={}\n", x); // prints "min=-" (should be -9223372036854775808) } ``` `u64.max` (via the NL.1 accessor, or any all-ones u64) prints `-1` for the same root reason. ## Root cause (suspected) The integer-to-string path is `s64`-based (`std.int_to_string` / the `{}` formatter takes `s64`): it negates the value to print the sign, but `-i64::MIN` overflows, and it has no unsigned-aware path so an all-ones u64 is read as `-1`. Needs a width/ signedness-aware integer formatter (format by the value's actual integer TYPE: unsigned types print the unsigned decimal; signed `MIN` is handled without negating). ## Investigation prompt Make the `{}` integer formatter type-aware: render an unsigned integer as its unsigned decimal (all 64 bits for u64), and handle signed `MIN` without the `-MIN` overflow (e.g. format the magnitude via unsigned arithmetic, or special-case MIN). Verify: `i64::MIN` prints `-9223372036854775808`; `u64.max` prints `18446744073709551615`; existing numeric output (incl. the NL.1 examples, which assert via bit-reinterpret) stays green. Likely area: the formatter / `int_to_string` in the std print path and/or the comptime `{}` lowering.