ADR-0046: Extended Numeric Types (isize/usize, f16/f32/f64, comptime_int)
Status
Implemented
Summary
Add the remaining Rust-equivalent numeric primitive types to Gruel: pointer-sized integers (isize, usize), IEEE 754 floating-point types (f16, f32, f64), and a compile-time integer type (comptime_int). This enables real-world systems programming workloads.
Context
Gruel currently supports i8/i16/i32/i64 and u8/u16/u32/u64. This covers common cases but leaves important gaps:
isize/usize: Required for safe array/slice indexing and memory-sized quantities. Today Gruel has no pointer-sized integer, making array indexing inherently un-portable across 32-bit and 64-bit targets.f16/f32/f64: Floating-point math is essential for scientific computing, graphics, game development, audio processing, and general-purpose programming. Without floats, Gruel cannot serve as a general-purpose language.f16is increasingly important for ML inference and GPU interop.
Design constraints
- The
Typeencoding uses a u32 with tag bits 0–13 for primitives and 100+ for composites. Adding new primitive tags fits cleanly within the existing scheme. - LLVM natively supports all of these types (
half,float,double, plus target-specificisize/usizemapping toi32ori64). - Floating-point introduces a new literal syntax (
3.14,1e10) and new semantic considerations (NaN, infinities, comparison semantics).
Decision
New types
| Gruel type | LLVM type | Size | Notes |
|---|---|---|---|
isize | i32 or i64 | target-dependent | Pointer-sized signed integer |
usize | i32 or i64 | target-dependent | Pointer-sized unsigned integer |
f16 | half | 2 bytes | IEEE 754 binary16 (half-precision) |
f32 | float | 4 bytes | IEEE 754 binary32 (single-precision) |
f64 | double | 8 bytes | IEEE 754 binary64 (double-precision) |
comptime_int | N/A (compile-time only) | 8 bytes (i64) | Compile-time integer, analogous to comptime_str |
comptime_int
comptime_int is a compile-time-only integer type, analogous to the existing comptime_str. It is the type of integer expressions evaluated at compile time within comptime blocks. Internally represented as an i64.
Key properties:
- Not a runtime type: Cannot appear in function signatures, struct fields, or variable types. It exists only during comptime evaluation.
- Coerces to any integer type: When a
comptime_intvalue flows into a runtime context, it is implicitly narrowed to the expected integer type (with a compile-time range check). - Follows the
comptime_strpattern: Added toTypeKindandTypeas a primitive tag. Cannot be interned in the type pool. Is Copy. - Use case: Enables comptime functions that compute indices, sizes, or other integer values that feed into runtime code — e.g.,
comptime { array_len * 2 }returning acomptime_intthat coerces tousize.
Literal syntax
Integer literals remain unchanged — 42 defaults to i32, type-inferred as today.
Floating-point literals use the syntax:
let a = 3.14; // f64 (default)
let b: f32 = 3.14; // f32 via type inference
let c = 1e10; // f64 (scientific notation)
let d = 1.5e-3; // f64 (scientific notation with decimal)
let e = 0.0; // f64
Floating-point literals default to f64 (matching Rust). A literal with a . or e/E is always floating-point. There is no suffix syntax (3.14f32) — use type annotations instead.
Disambiguating integers from floats: 42 is always an integer. 42.0 is always a float. 42.method() is a method call on integer 42 — the parser must handle this (see Implementation Phases).
Type encoding
Extend the primitive tag range in Type(u32):
| Tag | Type |
|---|---|
| 0–7 | (existing: i8..u64) |
| 8 | Isize |
| 9 | Usize |
| 10 | F16 |
| 11 | F32 |
| 12 | F64 |
| 13 | Bool |
| 14 | Unit |
| 15 | Error |
| 16 | Never |
| 17 | ComptimeType |
| 18 | ComptimeStr |
| 19 | ComptimeInt |
This requires updating:
TypeKindenumTypeconstants andkind()/try_kind()dispatchis_integer()(now 0–9 includes pointer-sized)- New
is_float()helper (10–12) - New
is_numeric()helper (is_integer() || is_float()) is_signed()(now includes Isize=8, and all floats)is_64_bit()(add U64, I64, Isize/Usize on 64-bit targets, F64)is_comptime_int()helper (tag 19), mirroringis_comptime_str()is_copy()(ComptimeInt is Copy, like ComptimeStr)
Arithmetic semantics
Integers (isize/usize): Follow existing Gruel semantics — overflow panics at runtime (both signed and unsigned). All existing integer operators (+, -, *, /, %, <<, >>, &, |, ^) work on these types.
Floating-point (f16/f32/f64): Support +, -, *, /, % (remainder), unary -. Do not support bitwise operators (&, |, ^, <<, >>).
Floating-point comparison: ==, !=, <, >, <=, >= use IEEE 754 semantics (NaN != NaN, NaN comparisons return false). No special NaN-handling syntax initially.
Floating-point overflow: Follows IEEE 754 — overflows produce infinity, not a panic. This differs from integer overflow behavior. Division by zero produces infinity (not a panic).
f16 precision note: f16 has very limited precision (about 3 decimal digits, range ±65504). It is primarily a storage/interchange format for ML and GPU workloads. Arithmetic on f16 is emitted as LLVM half operations — on targets without hardware f16 support, LLVM will promote to f32 for computation and convert back.
Casting
Extend @intCast or introduce a more general @cast intrinsic for:
- Integer ↔ integer (existing, extended to pointer-sized)
- Float → float (
f16↔f32↔f64) - Integer → float
- Float → integer (truncates toward zero; panics if value is NaN or out of range of the target integer type, matching Rust's checked
asbehavior since 1.45)
No implicit numeric coercions — all cross-type conversions require explicit casting. This is consistent with Gruel's existing strictness.
Target-dependent isize/usize
isize and usize are pointer-sized: 32 bits on 32-bit targets, 64 bits on 64-bit targets. This is determined at compile time from gruel-target.
isize/usizeare distinct types fromi32/i64— no implicit conversion- Array indexing will eventually require
usize(future work, not part of this ADR) @intCastworks betweenisize/usizeand fixed-width integers
Implementation Phases
Phase 1: Pointer-sized integers (isize/usize)
- Add
Isize/UsizetoTypeKind,Typeconstants (tags 8, 9) - Update all type helper methods (is_integer, is_signed, is_unsigned, is_copy, literal_fits, etc.)
- Add
isize/usizetokens to lexer - Update parser type parsing
- Update RIR and Sema for arithmetic/comparison
- Update
@intCastto handle isize/usize (fixed same-width same-sign codegen bug) - Add LLVM codegen: map to
i64on 64-bit targets - Update
type_byte_sizeandtype_alignment(8-byte on 64-bit targets) - Add spec section 3.1 paragraphs for isize/usize (3.1:23, 3.1:24)
- Add spec tests (17 tests covering arithmetic, casting, comparison, function calls)
- Preview-gated via
--preview extended_numeric_types
Phase 2: Floating-point types (f16/f32/f64) — lexer and type system
- Add
F16/F32/F64toTypeKind,Typeconstants (tags 10–12) - Add
is_float(),is_numeric()helpers - Update
is_signed(),is_copy(), etc. - Add
f16/f32/f64type keyword tokens to lexer - Add floating-point literal token: regex
[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?and[0-9]+[eE][+-]?[0-9]+ - Handle parser ambiguity:
42.method()vs42.0(logos naturally disambiguates — dot-digit is float, dot-ident is method call) - Update parser type parsing to recognize
f16/f32/f64 - Add
Float(u64)literal variant to AST, RIR, AIR, and CFG instruction data (store f64 bits as u64 for Eq/Copy compatibility) - Add
FloatLiteraltoInferTypefor type inference (defaults to f64 if unconstrained) - Add LLVM type mapping:
ctx.f16_type(),ctx.f32_type(),ctx.f64_type() - Update
type_byte_sizeandtype_alignment: f16 (2/2), f32 (4/4), f64 (8/8) - Add
FloatConstcodegen: LLVMconst_floatemission - Add spec section 3.11 for floating-point types
- Add spec tests (10 tests covering type declarations, literals, inference, copy, preview gate)
- Preview-gated via
--preview extended_numeric_types
Phase 3: Floating-point codegen and semantics
- Emit float arithmetic:
fadd,fsub,fmul,fdiv,fremfor all three widths - Emit float comparisons:
fcmpwith ordered predicates (OEQ, OLT, OGT, OLE, OGE) - Emit float negation:
fneg - Reject bitwise operators on float types in Sema (via
IsNumericvsIsIntegerconstraint split) - Add spec tests for float arithmetic, comparison, negation, bitwise rejection (19 new tests)
- Extend
@castfor float↔int, float↔float, and int↔float conversions (fptrunc/fpext/sitofp/uitofp/fptosi/fptoui) - Add spec tests for float casting, edge cases (NaN, infinity, negative zero) — 29 tests in
cast.toml - Add spec tests for f16 range limits
Phase 4: Compile-time integer type (comptime_int)
- Add
ComptimeInttoTypeKind,Typeconstant (tag 19) - Add
is_comptime_int()helper, updateis_copy(),is_valid_encoding() - Mirror
ComptimeStrhandling: cannot be interned in type pool, not a runtime type - Update comptime interpreter to produce
ComptimeIntfor integer expressions in comptime blocks - Implement coercion from
comptime_intto any integer type (with compile-time range validation) - Update
Display/Debug/name()to return"comptime_int" - Add spec tests for comptime_int usage and coercion (7 tests in
comptime.toml, spec paragraphs 4.14:80-84)
Phase 5: Polish and edge cases
- Update
Display/Debugimpls for new types (already handled in Phase 1-4) - Update fuzz targets to generate programs with new types (structured_compiler generates isize/usize/f16/f32/f64 programs with preview feature enabled)
- Decide comptime float evaluation strategy (deferred: float comptime evaluation not supported yet, only integer comptime is implemented)
- Ensure error messages mention new types correctly (verified: type mismatch and literal range errors display new type names)
- Update
EnumDef::discriminant_typeif needed for very large enums (no change needed: u8/u16/u32/u64 discriminants suffice)
Consequences
Positive
usizeunlocks portable array indexing (future work)- Floating-point enables scientific/graphics/game workloads
f16enables ML inference and GPU interop without external conversion
Negative
- Increases compiler complexity (more type variants to handle in every match)
- Float semantics are inherently complex (NaN propagation, comparison edge cases)
isize/usizeintroduce target-dependency into the type systemf16has no hardware support on most non-GPU targets — LLVM emits software promotion to f32
Neutral
- The Type encoding scheme has plenty of room for future types
- LLVM handles all the heavy lifting for code generation
- No syntax changes beyond new type keywords and float literals
Open Questions
Float literal suffixes: Should we support
3.14f32suffix syntax, or rely solely on type inference from annotations? (Current decision: no suffixes, use annotations.)NaN boxing or special NaN handling: Should Gruel expose
NaN/Infinityas named constants, or require0.0 / 0.0-style construction? (Suggest: addf32::NAN,f64::INFINITYetc. as associated constants in a future ADR.)isize/usizeas array index type: Should array indexing requireusize? Current arrays use integer expressions. This is a semantic change that affects existing code and should be a separate ADR.Comptime float: Should float expressions be evaluable at comptime? This adds complexity (f64 arithmetic at compile time). Could defer and only allow float literals, not comptime float math.
Future Work
chartype (Unicode scalar value, stored as u32)- Float-specific intrinsics (
@sqrt,@sin,@cos, etc.) or amathmodule - Functions that check if a float is
NaNorInfinity usizeas the required array index type- Wrapping/saturating arithmetic operators or methods for integers