270: Hexadecimal Floating-Point Constants

by Peter McGoron

Status

This SRFI is currently in draft status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-270@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Abstract

Floating-point numbers are usually stored in radix 2, but are written by users in radix 10. This SRFI introduces Scheme syntax for hexadecimal floating point constants based on C99’s syntax. They use radix 16 for writing the integer and fractional part, and a radix 10 exponent part that raises the whole value to a power of 2.

Issues

Rationale

Computers generally use radix-2 (binary) to store numbers, while humans use radix-10 (decimal). A terminating decimal may not be a terminating binary floating point number, and vice versa. This makes entering floating-point constants error-prone, and makes code that makes use of these constants difficult to read.

To express binary floating point exactly, one can use hexadecimal floating point constants, which are written in base 16 instead of base 10 and whose exponential parts raise the given values to a power of 2 instead of a power of 10. They precisely represent binary floating point numbers of any significand and exponent size.

Hexadecimal floating point constants are used in other languages. They were popularized by C99, and were subsequently added to the IEEE 754 floating point standard in 2008. Julia, Java, and Zig implement C’s hexadecimal floating point syntax.

This SRFI proposes hexadecimal floating point constants in a similar way to C’s syntax. When a hexadecimal number is input using #x, numbers after a decimal point will be read as fractional numbers in base 16. The character p is an exponent, which raises the number to a specified power of 2. Unlike C’s syntax, the p is optional.

Specification

Non-normative text is in small print.

Strong text is used to denote RFC 2119 requirements and recommendations.

Syntax

The following is written in Scheme's BNF, extended with the following: ⟨thing⟩? means zero or one of ⟨thing⟩.

The formal syntax of Scheme is modified to have the following new productions:

⟨ureal R
::= … | ⟨hex float body R⟩ ⟨hex float suffix⟩?
⟨hex float body 16⟩
::= ⟨uinteger 16⟩
| . ⟨digit 16⟩+
| ⟨digit 16⟩+ . ⟨digit 16⟩*
⟨hex float suffix⟩
::= p ⟨sign⟩? ⟨digit 10⟩+

The rules for reading an inexact number are modified such that p is considered an exponent marker. Hence, a hexadecimal number with . in it or p in it is read as inexact unless it was prefixed with #e.

Note that hexadecimal floats in code will need a #x in front of them, because in the formal grammar, #x is required to produce ⟨ureal 16⟩ and hence ⟨hex float body 16⟩. It is not required in places like string->number, where the radix is supplied explicitly.

The exponent marker is p because it is ambiguous whether e would be a decimal separator or a hex digit.

A hexadecimal floating point number is interpreted such that

xnxn−1x0 . x−1x−2x−mpd

where each x is a hexadecimal digit and d is a base 10 number, is equal to

(Xn×16n + Xn−1×16n−1 + … + X0 + X−1×16−1 + X−2×16−2 + … + X−m×16m)×2d

where Xi is the numerical value of the digit xi. If p is not supplied, d is 0.

R6RS Version

On R6RS systems, the above grammar is modified to be:

⟨ureal R⟩
::= … | ⟨hex float body R⟩ ⟨hex float suffix⟩? ⟨mantissa width⟩
⟨hex float suffix⟩
::= ⟨hex exponent marker⟩ ⟨exponent marker⟩? ⟨sign⟩? ⟨digit 10⟩+
⟨hex exponent marker⟩
::= p
| P

The R6RS considerations for numbers with ⟨exponent marker⟩ and ⟨mantissa width⟩ apply here.

This grammar is a superset of the previous grammar.

Possible Extensions

This section is non-normative.

Procedures

The procedure string->number must understand hexadecimal floats.

Some implementations extend string->number to allow for radices up to 36, encompassing all letters of the alphabet like the C function strtol. This may conflict with the usage of p as a exponent marker, but that is a general issue with very high bases: what is 1e5 in base 16, and what is +2i in base 19? This SRFI does not specify the behavior of implementation-dependent extensions.

(write-hexadecimal-float z [port])

Write an inexact complex number as a hexadecimal floating point number to port, which by default is (current-output-port). If the number is in a IEEE 754 binary floating point format, then the number must be represented in a certain form:

If the number is complex, then each component must be printed with the above rules.

If an implementation supports IEEE 754 binary floating point numbers, then such a number written with this procedure, when read using read on the same implementation, must be the same number according to eqv?. On implementations with multiple inexact real representations, this can be accomplished using the R6RS exponent markers.

If z cannot be represented as a finite hexadecimal float, then it is represented to an implementation defined precision. The implementation should write the closest inexact number to z.

No prefix (such as #x or #e) is written.

An implementation should output the minimum length string that satisfies these requirements.

Examples

This section is non-normative.

This section assumes that the suffix f represents a binary32 number, and d represents a binary64 number.

Expression Number
#x9p9 9×29 = 4608
#x1.2p3 (1 + 2×16−1)×23 = 9
#xFE.FFp1 (15×161 + 14 + 15×16−1 + 15×16−2)×21 = 65279/128 ≈ 509.992…
#xFE.FF 15×161 + 14 + 15×16−1 + 15×16−2 = 65279/256 ≈ 254.996…
#x-0.Ap-2 (10×16−1)×2−2 = −5/32 = −.15625
#x1.9p1+10p1i (1 + 9×16−1)×21 + (1×16 + 0)×21i = 25/8 + 32i
#x1.921fb54442d18pd+1 ≈ π
#x1.921fb54442d18pf+1 = #x1.921fb6pf+1 ≈ π
#x1p5@1p6 32@64 ≈ 12.539+29.440i

Implementation

A portable implementation is impossible, as it depends on both modifying the reader and knowing the specific representation of inexact reals.

An implementation written for Chez 10.4.0 is in the SRFI repository. The implementation includes a patch to the reader and test suite that automatically generates hex floats to test against.

The implementation of write-hexadecimal-float depends on a procedure decode-float, which is implemented using R6RS bytevector operations (assuming inexact reals are IEEE binary64). Such a procedure can be implemented using SRFI 144 flonums, using a procedure in the repository contributed by Sergei Egorov.

Acknowledgements

Taylor Campbell suggested it to me on #scheme.

© 2026 Peter McGoron.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Arthur A. Gleckler