fix(std): render integer formatter extremes — i64::MIN and unsigned all-ones [F0.8]
Resolves issue 0090. The `{}` integer formatter mis-rendered both ends of
the 64-bit range:
- `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 digit loop ran zero times, so only `-`
printed. It now extracts digits straight from `n` (per-digit
`|n % 10|`, `n` truncating toward zero), never negating MIN.
- `any_to_string`'s `case int:` formatted every integer as s64, so a u64
all-ones value printed as `-1`. There was no `uint` type-category to
distinguish signedness. Added an additive `type_is_unsigned(T)`
reflection builtin (static fold + dynamic interp/LLVM paths, mirroring
`type_name`), backed by the new `TypeTable.isUnsignedInt` predicate, and
a `uint_to_string` formatter (unsigned decimal via long-division over
four 16-bit limbs). `case int:` routes through `type_is_unsigned(type)`.
The 16-bit-limb split is factored into a shared `decompose_u16x4`, now
reused by `int_to_hex_string` (no second unsigned-math routine).
Regression: examples/0046-basic-int-formatter-extremes pins both extremes
plus a width spread; unit tests cover `isUnsignedInt`. Docs (specs.md
representation note, readme std API) updated for unsigned/extreme `{}`
behavior. IR snapshots refreshed for the two new std functions.
This commit is contained in:
83
issues/0090-int-formatter-extremes.md
Normal file
83
issues/0090-int-formatter-extremes.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user