Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
7.4 KiB
0090 — integer formatter can't render i64::MIN or unsigned all-ones
STATUS: RESOLVED (F0.8). Both extremes now render correctly:
i64.min→-9223372036854775808,u64.max→18446744073709551615.Root cause.
- Symptom 1 (i64::MIN):
std.int_to_stringcomputed the magnitude as0 - n, which overflows fori64::MIN(its magnitude is unrepresentable as a positive i64) — the value stayed negative, thewhile v > 0loop ran zero times, and only the-was emitted.- Symptom 2 (unsigned all-ones):
any_to_string'scase int:arm formatted every integer as i64 (int_to_string(xx val)); there was no way to tell au64from ani64, so an all-ones u64 printed as-1.Fix per file.
library/modules/std.sx—int_to_stringnow extracts digits straight fromn(taking|n % 10|per digit,ntruncates toward zero) so it never negatesi64::MIN. Addeduint_to_string(unsigned decimal via long-division-by-10 over four 16-bit limbs) anddecompose_u16x4(the shared 16-bit-limb split, now reused byint_to_hex_stringtoo).any_to_string'scase int:routes through the newtype_is_unsigned(type)query to pick the unsigned vs signed formatter. Declaredtype_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_unsignedBuiltinId.src/ir/calls.zig— registertype_is_unsignedas a.boolreflection builtin.src/ir/lower.zig—tryLowerReflectionCallarm: static fold + dynamiccallBuiltin.src/ir/interp.zig— interp arm (reads the boxed TypeId /type_ofaggregate shape).src/ir/emit_llvm.zig+src/backend/llvm/reflection.zig+src/backend/llvm/ops.zig— lazy[N x i1]__sx_type_is_unsignedtable built fromisUnsignedInt; runtime arm GEPs in at the TypeId.Regression test.
examples/0046-basic-int-formatter-extremes.sxpins both extremes plus a width spread (i8/i16/i32 + u8/u16/u32/u64, mins/maxes, 0, ordinary values). Unit tests:isUnsignedIntinsrc/ir/types.test.zig.Follow-up (F0.8 attempt 2) — strict
$T: Typeon all 7 reflection builtins. The stress-review of the additivetype_is_unsignedbuiltin found it (and the whole reflection family) silently accepted a non-type argument:type_is_unsigned(6)reinterpreted6as a TypeId index and returned the signedness oftypes[6](u8→ true);size_of(6)/(true)sized itstypeof(8);type_name(6)returnedtypes[6]'s name. Per Agra's ruling, all 7 type-introspection builtins —size_of,align_of,field_count,type_name,type_eq,type_is_unsigned,is_flags— now STRICTLY require a type (compile-time): a value argument is rejected with"<builtin> expects a type, got '<type>'".
src/ir/lower.zig— one shared guard,reflectionTypeArgGuard(run at the top oftryLowerReflectionCall), classifies each arg viareflectionArgIsType: a spelled / compile-time type or generic type param (theisStaticTypeArgshapes), or a runtimeTypevalue (static type.any—type_of(x), a[]Typeelementlist[i], aType-typed local / field / param) is ACCEPTED; anything else is rejected. The existing runtime path fortype_name/type_is_unsignedis preserved (the formatter callstype_is_unsigned(type_of(val))at runtime). The 5 comptime-only builtins stay comptime-only (runtime reflection deferred).- Negative regression:
examples/1144-diagnostics-reflection-builtin-needs-type.sx(reject cases across all 7, exit 1). Unit test:reflectionArgIsTypeinsrc/ir/lower.test.zig.Follow-up (F0.8 attempt 3) — reflection builtins on an
Anyconsult the Any's runtime TYPE-TAG, not its payload. The attempt-2 guard correctly accepts anAnyargument (the formatter passesval: Any), but the dynamictype_name/type_is_unsignedpath still read the Any's payload as a TypeId index unconditionally — correct only when the Any holds a Type value. For an Any holding a value (av : Any = 6, runtime tagi64, payload6) it reportedtypes[6](u8):type_name(av)→"u8",type_is_unsigned(av)→true. Per Agra's ruling ("Any is a type AND a value, so it's expected to work"), both builtins now branch on the Any's runtime tag: tag== .any→ the box is a Type value, use the payload as the TypeId; otherwise the tag IS the held value's type. Sotype_name(av)→"i64",type_is_unsigned(av)→false, whiletype_name(type_of(x))still names the held type. The formatter is unchanged (it already passedtype_of(val), a proper Type value).
src/ir/interp.zig— sharedValue.reflectTypeId(the tag-branching resolver); thetype_name/type_is_unsignedinterp arms route through it.src/backend/llvm/ops.zig— sharedOps.reflectArgTypeIdemitsextractvalue tag/icmp eq tag, .any/selectfor the runtime path; both reflection arms route through it. The two backends agree.- Regression:
examples/0164-types-reflection-any-tag.sxpinstype_name/type_is_unsigned/reflectTypeIdinsrc/ir/interp.test.zig.- Out of scope (kept comptime-only / deferred): the 5 comptime-only builtins (
size_of/align_of/field_count/is_flags/type_eq).type_eqhas no dynamic emit path (it folds at lower time), so it is unaffected.
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 i64-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)
#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 i64-based (std.int_to_string / the {} formatter
takes i64): 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.