blob: b22e6bb247cc140b85db970a06b7349fb9bd54fe [file]
// Copyright 2026 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// IREE's platform-independent printf implementation.
//
// Replaces the vendored eyalroz/printf with a focused implementation covering
// exactly the specifiers IREE uses. Every line is ours, every path is tested,
// every bounds check precedes its dereference.
//
// Supported specifiers:
// %d %i %u %o %x %X (integers, with hh/h/l/ll/z/t/j length modifiers)
// %s %c %p (string, character, pointer)
// %f %F %e %E %g %G (floating-point, double only)
// %% (literal percent)
//
// Supported flags: - + 0 # (space)
// Supported width/precision: literal and dynamic (*)
//
// NOT supported (by design):
// %n (writeback — security hazard)
// %a %A (hex float — 0 uses in IREE)
// %ls %lc (wide strings/chars — 0 uses in IREE)
// %L (long double — 0 uses in IREE)
// MSVC %I64d style — 0 uses in IREE
//
// Returns the number of characters that would have been written (excluding NUL)
// if the output were unbounded, or -1 on format error (unknown specifier).
#include "iree/base/printf.h"
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
//===----------------------------------------------------------------------===//
// Portable math (no libm dependency)
//===----------------------------------------------------------------------===//
// Provides fma, fmod, floor, and modf equivalents without requiring -lm.
// Uses hardware FMA when the compilation target has it (single instruction,
// no function call); falls back to the Dekker two-product algorithm otherwise.
#ifdef _MSC_VER
#include <math.h> // fma() — part of CRT on Windows, no separate libm.
#endif
// Hardware FMA detection: on these targets, __builtin_fma compiles to a single
// instruction with no libm dependency.
#if defined(__FMA__) || defined(__aarch64__) || defined(__ARM_FEATURE_FMA) || \
(defined(__riscv) && defined(__riscv_flen) && __riscv_flen >= 64)
#define IREE_PRINTF_HAS_HARDWARE_FMA 1
#else
#define IREE_PRINTF_HAS_HARDWARE_FMA 0
#endif
// Compute the exact rounding error of a*b: returns e such that
// a*b = product + e exactly (where product was computed as a*b by the FPU).
// On hardware-FMA targets, this is a single fused multiply-add instruction.
// On other targets, uses the Dekker-Veltkamp two-product algorithm, which
// computes the same result using only basic arithmetic. The Dekker path is
// safe from unintended FP contraction because it only activates on targets
// without FMA hardware (where the compiler cannot contract multiplications
// into FMA instructions).
static inline double iree_printf_mul_error(double a, double b, double product) {
#if IREE_PRINTF_HAS_HARDWARE_FMA
return __builtin_fma(a, b, -product);
#elif defined(_MSC_VER)
// MSVC: fma is in the CRT, no separate libm.
return fma(a, b, -product);
#else
// Dekker-Veltkamp two-product: split each operand into high/low halves
// (each with <= 26 mantissa bits), then compute the exact product error
// from the four partial products.
const double VELTKAMP_SPLIT = 134217729.0; // 2^27 + 1
double a_big = VELTKAMP_SPLIT * a;
double a_hi = a_big - (a_big - a);
double a_lo = a - a_hi;
double b_big = VELTKAMP_SPLIT * b;
double b_hi = b_big - (b_big - b);
double b_lo = b - b_hi;
return ((a_hi * b_hi - product) + a_hi * b_lo + a_lo * b_hi) + a_lo * b_lo;
#endif // FMA
}
// Portable floor: returns the largest integer <= x.
// Only called on positive, finite values in our formatting paths.
static inline double iree_printf_floor(double x) {
// Doubles >= 2^52 in magnitude are always integers (the significand cannot
// represent a fractional part at that scale).
if (x >= 4503599627370496.0) return x;
if (x <= -4503599627370496.0) return x;
double truncated = (double)(int64_t)x;
// C truncation rounds toward zero; floor rounds toward -infinity.
if (truncated > x) truncated -= 1.0;
return truncated;
}
// Portable modf: split value into integral and fractional parts.
// Only called on positive, finite values in our formatting paths.
static inline double iree_printf_modf(double value, double* integral_part) {
if (value >= 4503599627370496.0 || value <= -4503599627370496.0) {
*integral_part = value;
return 0.0;
}
double truncated = (double)(int64_t)value;
*integral_part = truncated;
return value - truncated;
}
// Check if an integer-valued double is odd. Used for banker's rounding.
// Doubles >= 2^53 in magnitude always represent even integers (the lowest
// bit of the significand corresponds to 2 or more at that scale).
static inline bool iree_printf_is_odd(double x) {
if (x >= 9007199254740992.0 || x <= -9007199254740992.0) return false;
return ((int64_t)x) & 1;
}
//===----------------------------------------------------------------------===//
// Output abstraction
//===----------------------------------------------------------------------===//
// Handles three output modes through a single interface:
// 1. Buffer mode: writes to a sized char buffer with truncation.
// 2. Callback mode: calls a per-character callback for each output character.
// 3. Dry-run mode: counts characters without writing (NULL buffer, no
// callback).
// Position is always tracked, even past the buffer capacity, so the return
// value correctly reports the total formatted length.
typedef struct {
char* buffer;
size_t capacity;
size_t position;
iree_printf_callback_t callback;
void* callback_data;
} iree_printf_output_t;
static inline void iree_printf_output_char(iree_printf_output_t* out, char c) {
if (out->callback) {
out->callback(c, out->callback_data);
} else if (out->buffer && out->position < out->capacity) {
out->buffer[out->position] = c;
}
out->position++;
}
// Emit |count| copies of |c|.
static void iree_printf_output_fill(iree_printf_output_t* out, char c,
size_t count) {
for (size_t i = 0; i < count; i++) {
iree_printf_output_char(out, c);
}
}
// Emit |length| bytes from |str|. The caller must guarantee |str| points to at
// least |length| readable bytes.
static void iree_printf_output_string(iree_printf_output_t* out,
const char* str, size_t length) {
for (size_t i = 0; i < length; i++) {
iree_printf_output_char(out, str[i]);
}
}
//===----------------------------------------------------------------------===//
// Format specifier parsing
//===----------------------------------------------------------------------===//
// Flags that modify formatting behavior.
#define IREE_PRINTF_FLAG_LEFT (1u << 0) // '-': left-justify within width.
#define IREE_PRINTF_FLAG_PLUS (1u << 1) // '+': show sign for positive.
#define IREE_PRINTF_FLAG_SPACE (1u << 2) // ' ': space before positive.
#define IREE_PRINTF_FLAG_ZERO (1u << 3) // '0': zero-pad within width.
#define IREE_PRINTF_FLAG_HASH (1u << 4) // '#': alternate form (0x, etc).
// Length modifier encoding.
typedef enum {
IREE_PRINTF_LENGTH_NONE = 0,
IREE_PRINTF_LENGTH_HH, // char / unsigned char (promoted to int in va_arg).
IREE_PRINTF_LENGTH_H, // short / unsigned short (promoted to int).
IREE_PRINTF_LENGTH_L, // long / unsigned long.
IREE_PRINTF_LENGTH_LL, // long long / unsigned long long.
IREE_PRINTF_LENGTH_Z, // size_t / ssize_t.
IREE_PRINTF_LENGTH_T, // ptrdiff_t.
IREE_PRINTF_LENGTH_J, // intmax_t / uintmax_t.
} iree_printf_length_t;
// Parsed format specifier. Extracted from the format string between '%' and the
// conversion character.
typedef struct {
uint32_t flags;
int width;
int precision;
bool has_precision;
iree_printf_length_t length;
char specifier;
} iree_printf_spec_t;
// Parse flags from the format string. Returns pointer past the last flag.
static const char* iree_printf_parse_flags(const char* format,
uint32_t* out_flags) {
uint32_t flags = 0;
for (;;) {
switch (*format) {
case '-':
flags |= IREE_PRINTF_FLAG_LEFT;
break;
case '+':
flags |= IREE_PRINTF_FLAG_PLUS;
break;
case ' ':
flags |= IREE_PRINTF_FLAG_SPACE;
break;
case '0':
flags |= IREE_PRINTF_FLAG_ZERO;
break;
case '#':
flags |= IREE_PRINTF_FLAG_HASH;
break;
default:
*out_flags = flags;
return format;
}
format++;
}
}
// Parse an unsigned decimal integer from the format string.
// Returns pointer past the last digit. |out_value| is unchanged if no digits.
// Values are clamped to avoid signed integer overflow (undefined behavior).
// The clamp limit is generous enough for any legitimate width/precision while
// preventing UB from malicious or malformed format strings.
#define IREE_PRINTF_MAX_WIDTH_PRECISION 10000
static const char* iree_printf_parse_uint(const char* format, int* out_value) {
if (*format < '0' || *format > '9') return format;
int value = 0;
while (*format >= '0' && *format <= '9') {
if (value <= IREE_PRINTF_MAX_WIDTH_PRECISION) {
value = value * 10 + (*format - '0');
}
format++;
}
if (value > IREE_PRINTF_MAX_WIDTH_PRECISION) {
value = IREE_PRINTF_MAX_WIDTH_PRECISION;
}
*out_value = value;
return format;
}
// Parse a complete format specifier starting after the '%'.
// Returns pointer past the specifier character, or NULL on error.
static const char* iree_printf_parse_spec(const char* format, va_list* args,
iree_printf_spec_t* out_spec) {
memset(out_spec, 0, sizeof(*out_spec));
out_spec->precision = -1; // Sentinel: no precision specified.
// Flags.
format = iree_printf_parse_flags(format, &out_spec->flags);
// Width: literal or '*' (read from args).
if (*format == '*') {
out_spec->width = va_arg(*args, int);
if (out_spec->width < 0) {
// Negative width means left-justify with the absolute value.
// Cast to unsigned before negation to handle INT_MIN without UB.
out_spec->flags |= IREE_PRINTF_FLAG_LEFT;
out_spec->width = (int)(-(unsigned int)out_spec->width);
if (out_spec->width < 0) out_spec->width = 0; // INT_MIN edge case.
}
if (out_spec->width > IREE_PRINTF_MAX_WIDTH_PRECISION) {
out_spec->width = IREE_PRINTF_MAX_WIDTH_PRECISION;
}
format++;
} else {
format = iree_printf_parse_uint(format, &out_spec->width);
}
// Precision: '.' followed by literal or '*'.
if (*format == '.') {
format++;
out_spec->has_precision = true;
out_spec->precision = 0; // Default precision after '.' is 0.
if (*format == '*') {
out_spec->precision = va_arg(*args, int);
if (out_spec->precision < 0) {
// Negative precision is treated as if precision were omitted.
out_spec->has_precision = false;
out_spec->precision = -1;
} else if (out_spec->precision > IREE_PRINTF_MAX_WIDTH_PRECISION) {
out_spec->precision = IREE_PRINTF_MAX_WIDTH_PRECISION;
}
format++;
} else {
format = iree_printf_parse_uint(format, &out_spec->precision);
}
}
// Length modifier.
switch (*format) {
case 'h':
format++;
if (*format == 'h') {
out_spec->length = IREE_PRINTF_LENGTH_HH;
format++;
} else {
out_spec->length = IREE_PRINTF_LENGTH_H;
}
break;
case 'l':
format++;
if (*format == 'l') {
out_spec->length = IREE_PRINTF_LENGTH_LL;
format++;
} else {
out_spec->length = IREE_PRINTF_LENGTH_L;
}
break;
case 'z':
out_spec->length = IREE_PRINTF_LENGTH_Z;
format++;
break;
case 't':
out_spec->length = IREE_PRINTF_LENGTH_T;
format++;
break;
case 'j':
out_spec->length = IREE_PRINTF_LENGTH_J;
format++;
break;
default:
break;
}
// Specifier character.
out_spec->specifier = *format;
if (*format == '\0') return NULL; // Truncated format string.
return format + 1;
}
//===----------------------------------------------------------------------===//
// Integer formatting
//===----------------------------------------------------------------------===//
// Handles %d, %i, %u, %o, %x, %X with all length modifiers and flags.
//
// Approach: extract the value as uint64_t (with sign handling for signed
// specifiers), convert digits into a reverse buffer, then emit with
// padding/prefix/sign.
//
// INT64_MIN correctness: for signed specifiers, the value is cast to unsigned
// BEFORE negation. This avoids undefined behavior from negating the most
// negative value (-(-2^63) overflows signed int64_t, but works as unsigned).
// Maximum digits: 64 binary digits, or 22 octal digits for UINT64_MAX.
// We also need space for the prefix (0x) and sign, but those are handled
// separately — this buffer is just for the digit characters.
#define IREE_PRINTF_INT_BUFFER_SIZE 64
static void iree_printf_format_integer(iree_printf_output_t* out,
const iree_printf_spec_t* spec,
va_list* args) {
// Determine base and digit case from specifier.
unsigned int base = 10;
const char* digits = "0123456789abcdef";
switch (spec->specifier) {
case 'o':
base = 8;
digits = "0123456789abcdef";
break;
case 'x':
base = 16;
digits = "0123456789abcdef";
break;
case 'X':
base = 16;
digits = "0123456789ABCDEF";
break;
default: // d, i, u
base = 10;
digits = "0123456789abcdef";
break;
}
bool is_signed = (spec->specifier == 'd' || spec->specifier == 'i');
// Extract the value from va_args as the correct type. For signed specifiers,
// track the sign separately and work with the absolute value as unsigned.
bool is_negative = false;
uint64_t value = 0;
if (is_signed) {
int64_t signed_value = 0;
switch (spec->length) {
case IREE_PRINTF_LENGTH_HH:
// char promoted to int.
signed_value = (signed char)va_arg(*args, int);
break;
case IREE_PRINTF_LENGTH_H:
// short promoted to int.
signed_value = (short)va_arg(*args, int);
break;
case IREE_PRINTF_LENGTH_L:
signed_value = va_arg(*args, long);
break;
case IREE_PRINTF_LENGTH_LL:
signed_value = va_arg(*args, long long);
break;
case IREE_PRINTF_LENGTH_Z:
// The 'z' modifier with a signed specifier (%zd) expects ssize_t,
// which is POSIX-only. Use ptrdiff_t (C99) as a portable equivalent —
// both are the signed counterpart of size_t on all IREE targets.
signed_value = va_arg(*args, ptrdiff_t);
break;
case IREE_PRINTF_LENGTH_T:
signed_value = va_arg(*args, ptrdiff_t);
break;
case IREE_PRINTF_LENGTH_J:
signed_value = va_arg(*args, intmax_t);
break;
default:
signed_value = va_arg(*args, int);
break;
}
if (signed_value < 0) {
is_negative = true;
// Cast to unsigned BEFORE negation to handle INT64_MIN.
value = (uint64_t)(-(uint64_t)signed_value);
} else {
value = (uint64_t)signed_value;
}
} else {
switch (spec->length) {
case IREE_PRINTF_LENGTH_HH:
value = (unsigned char)va_arg(*args, unsigned int);
break;
case IREE_PRINTF_LENGTH_H:
value = (unsigned short)va_arg(*args, unsigned int);
break;
case IREE_PRINTF_LENGTH_L:
value = va_arg(*args, unsigned long);
break;
case IREE_PRINTF_LENGTH_LL:
value = va_arg(*args, unsigned long long);
break;
case IREE_PRINTF_LENGTH_Z:
value = va_arg(*args, size_t);
break;
case IREE_PRINTF_LENGTH_T:
// ptrdiff_t treated as unsigned for %u/%x/%o.
value = (uint64_t)va_arg(*args, ptrdiff_t);
break;
case IREE_PRINTF_LENGTH_J:
value = va_arg(*args, uintmax_t);
break;
default:
value = va_arg(*args, unsigned int);
break;
}
}
// Convert value to digit characters in reverse order.
// Track whether the original value was zero — this affects '#' prefix logic
// (the C standard says '#' does not add 0x/0 prefix for zero).
bool is_zero = (value == 0);
char digit_buffer[IREE_PRINTF_INT_BUFFER_SIZE];
int digit_count = 0;
if (value == 0) {
// Special case: zero. If precision is explicitly 0, no digits are emitted —
// EXCEPT for octal with '#' flag, where C99 §7.19.6.1p6 requires:
// "if the value and precision are both 0, a single 0 is printed."
if (spec->has_precision && spec->precision == 0) {
if ((spec->flags & IREE_PRINTF_FLAG_HASH) && base == 8) {
digit_buffer[digit_count++] = '0';
}
} else {
digit_buffer[digit_count++] = '0';
}
} else {
while (value > 0) {
digit_buffer[digit_count++] = digits[value % base];
value /= base;
}
}
// Determine the precision-padded digit count (precision specifies minimum
// number of digits for integers).
int precision = spec->has_precision ? spec->precision : 1;
int padded_digit_count = digit_count > precision ? digit_count : precision;
// Determine the prefix: sign character for signed specifiers, '0x'/'0X' for
// hex with '#' flag, or combinations thereof.
const char* prefix = "";
int prefix_length = 0;
if ((spec->flags & IREE_PRINTF_FLAG_HASH) && base == 16 && !is_zero) {
// Hex with '#' flag: "0x"/"0X" prefix.
// C standard: '#' has no effect on zero value.
// Note: %x/%X are unsigned specifiers, so no sign prefix is needed.
prefix = (spec->specifier == 'X') ? "0X" : "0x";
prefix_length = 2;
} else if ((spec->flags & IREE_PRINTF_FLAG_HASH) && base == 8 && !is_zero) {
// Octal '#': ensure there's a leading zero. If precision already provides
// one, no additional zero is needed. The value==0 case is handled earlier
// in the digit emission (a single '0' is always emitted for #o with 0).
// Note: %o is an unsigned specifier, so no sign prefix is needed.
if (padded_digit_count <= digit_count) {
padded_digit_count = digit_count + 1;
}
} else {
// No '#' hex/octal prefix — just sign.
if (is_negative) {
prefix = "-";
prefix_length = 1;
} else if (is_signed && (spec->flags & IREE_PRINTF_FLAG_PLUS)) {
prefix = "+";
prefix_length = 1;
} else if (is_signed && (spec->flags & IREE_PRINTF_FLAG_SPACE)) {
prefix = " ";
prefix_length = 1;
}
}
// Total content width: prefix + precision-padded digits.
int content_width = prefix_length + padded_digit_count;
// Determine padding. Zero-padding is only used when:
// - '0' flag is set
// - '-' flag is NOT set (left-justify overrides zero-pad)
// - No precision is specified (precision overrides zero-pad for integers)
bool use_zero_pad = (spec->flags & IREE_PRINTF_FLAG_ZERO) &&
!(spec->flags & IREE_PRINTF_FLAG_LEFT) &&
!spec->has_precision;
int padding = 0;
if (spec->width > content_width) {
padding = spec->width - content_width;
}
if (use_zero_pad) {
// Zero padding goes between the prefix and digits.
iree_printf_output_string(out, prefix, prefix_length);
iree_printf_output_fill(out, '0', padding);
} else if (!(spec->flags & IREE_PRINTF_FLAG_LEFT)) {
// Right-justify: space padding before prefix.
iree_printf_output_fill(out, ' ', padding);
iree_printf_output_string(out, prefix, prefix_length);
} else {
// Left-justify: prefix first, padding at the end.
iree_printf_output_string(out, prefix, prefix_length);
}
// Precision zero-padding (minimum digits).
int precision_zeros = padded_digit_count - digit_count;
iree_printf_output_fill(out, '0', precision_zeros);
// Digits in forward order (they're stored in reverse).
for (int i = digit_count - 1; i >= 0; i--) {
iree_printf_output_char(out, digit_buffer[i]);
}
// Left-justify trailing spaces.
if (spec->flags & IREE_PRINTF_FLAG_LEFT) {
iree_printf_output_fill(out, ' ', padding);
}
}
//===----------------------------------------------------------------------===//
// String, character, and pointer formatting
//===----------------------------------------------------------------------===//
static void iree_printf_format_string(iree_printf_output_t* out,
const iree_printf_spec_t* spec,
va_list* args) {
const char* str = va_arg(*args, const char*);
if (!str) str = "(null)";
// Determine the output length. Precision limits the number of bytes read
// from the string — the bounds check MUST precede the dereference.
size_t length = 0;
if (spec->has_precision) {
// Read at most |precision| bytes. Check the bound BEFORE dereferencing.
size_t max_length = (size_t)spec->precision;
while (length < max_length && str[length] != '\0') {
length++;
}
} else {
length = strlen(str);
}
// Padding.
int padding = 0;
if (spec->width > 0 && (size_t)spec->width > length) {
padding = spec->width - (int)length;
}
if (!(spec->flags & IREE_PRINTF_FLAG_LEFT)) {
iree_printf_output_fill(out, ' ', padding);
}
iree_printf_output_string(out, str, length);
if (spec->flags & IREE_PRINTF_FLAG_LEFT) {
iree_printf_output_fill(out, ' ', padding);
}
}
static void iree_printf_format_char(iree_printf_output_t* out,
const iree_printf_spec_t* spec,
va_list* args) {
char c = (char)va_arg(*args, int);
int padding = 0;
if (spec->width > 1) {
padding = spec->width - 1;
}
if (!(spec->flags & IREE_PRINTF_FLAG_LEFT)) {
iree_printf_output_fill(out, ' ', padding);
}
iree_printf_output_char(out, c);
if (spec->flags & IREE_PRINTF_FLAG_LEFT) {
iree_printf_output_fill(out, ' ', padding);
}
}
static void iree_printf_format_pointer(iree_printf_output_t* out,
const iree_printf_spec_t* spec,
va_list* args) {
uintptr_t ptr = (uintptr_t)va_arg(*args, void*);
// Format as "0x" followed by lowercase hex digits. No leading zeros beyond
// what the value requires (matching glibc behavior).
// For NULL, we still print "0x0".
char digit_buffer[IREE_PRINTF_INT_BUFFER_SIZE];
int digit_count = 0;
if (ptr == 0) {
digit_buffer[digit_count++] = '0';
} else {
while (ptr > 0) {
digit_buffer[digit_count++] = "0123456789abcdef"[ptr & 0xF];
ptr >>= 4;
}
}
int content_width = 2 + digit_count; // "0x" + digits.
int padding = 0;
if (spec->width > content_width) {
padding = spec->width - content_width;
}
if (!(spec->flags & IREE_PRINTF_FLAG_LEFT)) {
iree_printf_output_fill(out, ' ', padding);
}
iree_printf_output_string(out, "0x", 2);
for (int i = digit_count - 1; i >= 0; i--) {
iree_printf_output_char(out, digit_buffer[i]);
}
if (spec->flags & IREE_PRINTF_FLAG_LEFT) {
iree_printf_output_fill(out, ' ', padding);
}
}
//===----------------------------------------------------------------------===//
// Floating-point formatting
//===----------------------------------------------------------------------===//
// Handles %f, %F, %e, %E, %g, %G for double values.
//
// Precision: we do NOT need shortest-round-trip representation (Grisu/Ryu).
// We need correctness to the displayed precision with correct rounding.
//
// Approach: IEEE 754 bit extraction for sign/special values, then arithmetic
// decomposition into integral + fractional parts using int64_t digit
// extraction. This is accurate to 17 significant digits (full double
// precision). Precision is capped at 17; beyond that we pad with zeros.
// Maximum precision we compute accurately. Beyond this, we emit zeros.
// 17 significant digits covers the full precision of IEEE 754 double.
#define IREE_PRINTF_MAX_FLOAT_PRECISION 17
// Maximum effective precision for the fixed-point fractional digit path. For
// small fractional values (e.g., 0.00000001) the effective precision is
// extended beyond IREE_PRINTF_MAX_FLOAT_PRECISION because the leading zeros
// don't consume significant digits. Capped at 19 to keep 10^n within uint64_t.
#define IREE_PRINTF_MAX_FIXED_FRACTION_DIGITS 19
// Maximum digits in the integral part of a double. DBL_MAX is ~1.8e308,
// so 309 digits maximum. We use a buffer large enough for this plus some
// margin for the decimal point, sign, and exponent notation.
#define IREE_PRINTF_FLOAT_BUFFER_SIZE 330
// IEEE 754 double-precision bit layout.
typedef union {
double value;
uint64_t bits;
} iree_printf_double_bits_t;
static inline bool iree_printf_double_is_negative(double value) {
iree_printf_double_bits_t u = {0};
u.value = value;
return (u.bits >> 63) != 0;
}
static inline bool iree_printf_double_is_nan(double value) {
iree_printf_double_bits_t u = {0};
u.value = value;
uint64_t exponent = (u.bits >> 52) & 0x7FF;
uint64_t mantissa = u.bits & 0x000FFFFFFFFFFFFFull;
return exponent == 0x7FF && mantissa != 0;
}
static inline bool iree_printf_double_is_inf(double value) {
iree_printf_double_bits_t u = {0};
u.value = value;
uint64_t exponent = (u.bits >> 52) & 0x7FF;
uint64_t mantissa = u.bits & 0x000FFFFFFFFFFFFFull;
return exponent == 0x7FF && mantissa == 0;
}
// Compute floor(log10(value)) for positive, finite, non-zero values.
// Uses the base-2 exponent from IEEE 754 bits and a correction factor.
// Accuracy: exact for most values; may be off by 1, which the caller corrects.
//
// Subnormals require special handling: the biased exponent is 0 for ALL
// subnormals (spanning ~5e-324 to ~2.2e-308), so the normal approximation
// collapses them all to the same estimate. For subnormals, we find the
// highest set bit in the mantissa to compute the effective binary exponent.
static int iree_printf_log10_approx(double value) {
iree_printf_double_bits_t u = {0};
u.value = value;
int exponent_biased = (int)((u.bits >> 52) & 0x7FF);
int effective_log2 = 0;
if (exponent_biased == 0) {
// Subnormal: value = 2^(-1022) * (mantissa / 2^52).
// Effective binary exponent = -1074 + position_of_highest_set_bit.
uint64_t mantissa = u.bits & 0x000FFFFFFFFFFFFFull;
if (mantissa == 0) return 0; // ±0, shouldn't reach here.
// Count leading zeros in the 52-bit mantissa field.
// Start from bit 51 (MSB of mantissa) and count down.
int highest_bit = 0;
for (int bit = 51; bit >= 0; bit--) {
if (mantissa & (1ull << bit)) {
highest_bit = bit;
break;
}
}
effective_log2 = -1074 + highest_bit;
} else {
effective_log2 = exponent_biased - 1023;
}
// log10(2) ≈ 0.30103. Use integer arithmetic: log10 ≈ log2 * 301 / 1000.
// For negative values, C integer division truncates toward zero, which gives
// us ceil(log10) instead of floor(log10). Correct by subtracting 1 when the
// product is negative and not evenly divisible.
int product = effective_log2 * 301;
int log10_estimate = product / 1000;
if (product < 0 && product % 1000 != 0) {
log10_estimate--;
}
return log10_estimate;
}
// Powers of 10 as doubles, for exponents 0..22. Beyond 22, 10^n is not exactly
// representable as a double, but we only need this for normalization where
// small errors are acceptable (they affect the last displayed digit at most).
static const double kPowersOf10[] = {
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
};
static double iree_printf_pow10(int n) {
// Keep the table lookup behind a simple checked unsigned magnitude. GCC 11 at
// -O3 can otherwise warn with a false-positive -Warray-bounds after inlining
// and constprop when it reasons about wrapped unsigned values.
unsigned int abs_n = n < 0 ? -(unsigned int)n : (unsigned int)n;
if (abs_n <= 22) {
return n < 0 ? 1.0 / kPowersOf10[abs_n] : kPowersOf10[abs_n];
}
// For large exponents, use iterative multiplication/division.
// This loses precision in the last few digits but is fine for our use case
// (debug formatting at precision <= 17).
double result = 1.0;
if (n > 0) {
double base = 10.0;
int exp = n;
while (exp > 0) {
if (exp & 1) result *= base;
base *= base;
exp >>= 1;
}
} else {
double base = 10.0;
int exp = -n;
while (exp > 0) {
if (exp & 1) result /= base;
base *= base;
exp >>= 1;
}
}
return result;
}
// Compute value * 10^n with FMA-corrected error tracking (double-double).
// Stores the high part in *out_result and returns the low part (error term),
// such that *out_result + return_value = value * 10^n to extended precision.
//
// For n <= 22, pow10(n) is exactly representable as a double, so a single FMA
// gives the exact error of the multiplication.
//
// For 22 < n <= 44, the multiplication is split into two exact steps:
// value * 10^n = (value * 10^22) * 10^(n-22)
// Both 10^22 and 10^(n-22) are exactly representable (both <= 10^22). FMA
// error terms from each step are combined for double-double precision.
//
// For n > 44, falls back to single-step with inexact pow10 (best-effort).
static double iree_printf_mul_pow10_error(double value, int n,
double* out_result) {
if (n <= 22) {
double scale = iree_printf_pow10(n);
*out_result = value * scale;
return iree_printf_mul_error(value, scale, *out_result);
} else if (n <= 44) {
// Split 10^n = 10^22 * 10^(n-22) where both factors are exact doubles.
double scale_first = iree_printf_pow10(22);
double scale_second = iree_printf_pow10(n - 22);
// Step 1: value * 10^22 with error tracking.
double partial = value * scale_first;
double error_first = iree_printf_mul_error(value, scale_first, partial);
// Step 2: partial * 10^(n-22) with error tracking.
*out_result = partial * scale_second;
double error_second =
iree_printf_mul_error(partial, scale_second, *out_result);
// Combined error: the second step's error plus the first step's error
// propagated through the second multiplication.
return error_second + error_first * scale_second;
} else {
// n > 44: pow10(n) is not exactly representable, so error correction
// handles only the multiplication rounding, not the scale factor.
// Best-effort.
double scale = iree_printf_pow10(n);
*out_result = value * scale;
return iree_printf_mul_error(value, scale, *out_result);
}
}
// Format a non-negative, finite double in %f style (fixed-point notation).
// Writes computed digits into |buffer| and returns the number of characters
// written. |buffer| must have at least IREE_PRINTF_FLOAT_BUFFER_SIZE bytes.
//
// Trailing fractional zeros beyond the computational precision limit (17
// significant digits) are NOT written to the buffer. Instead, their count is
// stored in |*out_trailing_zeros|. The caller must emit these zeros directly
// to the output after the buffer contents. This prevents stack buffer overflow
// when precision is large (e.g., %.400f would write 400 bytes of zeros into a
// 330-byte buffer without this separation).
static int iree_printf_format_fixed(char* buffer, double value, int precision,
bool force_decimal_point,
int* out_trailing_zeros) {
*out_trailing_zeros = 0;
int position = 0;
// Split into integral and fractional parts.
double integral_part = 0.0;
double fractional_part = iree_printf_modf(value, &integral_part);
// Clamp precision to what we can compute accurately. The base limit is 17
// significant digits (full double precision), but for fractional parts with
// leading zeros (e.g., 0.0005...), the precision in "digits after decimal
// point" includes leading zeros that don't consume significant digits. In
// those cases, higher effective_precision is safe because the scaled result
// (frac * 10^n) is still small enough to fit exactly in double (< 2^53).
int effective_precision = precision;
if (effective_precision > IREE_PRINTF_MAX_FLOAT_PRECISION) {
effective_precision = IREE_PRINTF_MAX_FLOAT_PRECISION;
// Extend precision for small fractional parts where the scaled result
// still fits in double precision (< 2^53 ~= 9e15). Also cap at 19 to
// prevent 10^effective_precision from overflowing uint64_t.
if (fractional_part > 0.0) {
while (effective_precision < precision &&
effective_precision < IREE_PRINTF_MAX_FIXED_FRACTION_DIGITS) {
double next_scale = iree_printf_pow10(effective_precision + 1);
if (fractional_part * next_scale >= 9.0e15) break;
effective_precision++;
}
}
}
// Convert fractional part to integer digits by multiplying by 10^precision.
// This gives us the fractional digits as an integer that we can extract.
double frac_scale = iree_printf_pow10(effective_precision);
double frac_scaled = fractional_part * frac_scale;
uint64_t frac_max = (uint64_t)frac_scale;
// Round with banker's rounding (round-half-to-even). When the fractional
// remainder is exactly 0.5, round to the nearest even digit. The "digit"
// that matters is the last displayed digit: the lowest fractional digit if
// precision > 0, or the ones digit of the integral part if precision == 0.
//
// Correct the multiplication rounding error: the product frac * 10^n may
// round to a value with remainder exactly 0.5 when the true remainder is
// near but not equal to 0.5.
double mul_error =
iree_printf_mul_error(fractional_part, frac_scale, frac_scaled);
uint64_t frac_digits = (uint64_t)frac_scaled;
double remainder = (frac_scaled - (double)frac_digits) + mul_error;
if (remainder > 0.5) {
frac_digits++;
} else if (remainder == 0.5) {
// Exactly half: determine which digit to check for evenness.
if (effective_precision > 0) {
// The last fractional digit determines rounding.
if (frac_digits & 1) frac_digits++;
} else {
// Precision is 0: the ones digit of the integral part determines
// rounding. frac_digits is 0 here (precision 0 means no frac digits).
uint64_t int_ones = (uint64_t)integral_part % 10;
if (int_ones & 1) frac_digits++; // Will carry into integral_part.
}
}
// Check for carry from rounding: if frac_digits == 10^precision, we need to
// increment the integral part.
if (frac_digits >= frac_max) {
frac_digits = 0;
integral_part += 1.0;
}
// Format the integral part.
if (integral_part == 0.0) {
buffer[position++] = '0';
} else if (integral_part < 18446744073709551616.0) { // < 2^64
// Value fits in uint64_t — extract digits directly for full precision.
char int_digits[IREE_PRINTF_FLOAT_BUFFER_SIZE];
int int_count = 0;
uint64_t int_value = (uint64_t)integral_part;
while (int_value > 0) {
int_digits[int_count++] = '0' + (char)(int_value % 10);
int_value /= 10;
}
for (int i = int_count - 1; i >= 0; i--) {
buffer[position++] = int_digits[i];
}
} else {
// Very large float (> UINT64_MAX, ~1.8e19). IEEE 754 double has at most
// 17 significant digits; beyond that the digits are determined by the
// binary-to-decimal conversion, not the original value. We extract the
// significant digits via normalization and pad the rest with zeros —
// this matches what libc printf does.
int exponent = iree_printf_log10_approx(integral_part);
double normalized = integral_part / iree_printf_pow10(exponent);
// Correct approximation.
if (normalized >= 10.0) {
normalized /= 10.0;
exponent++;
} else if (normalized < 1.0) {
normalized *= 10.0;
exponent--;
}
// Extract up to 17 significant digits from the normalized value.
int sig_digits = IREE_PRINTF_MAX_FLOAT_PRECISION;
if (sig_digits > exponent + 1) sig_digits = exponent + 1;
// Scale to get all significant digits as an integer.
double scaled = normalized * iree_printf_pow10(sig_digits - 1);
uint64_t sig_value = (uint64_t)(scaled + 0.5);
// Extract digits in reverse.
char sig_buffer[IREE_PRINTF_MAX_FLOAT_PRECISION + 1];
int sig_count = 0;
while (sig_value > 0) {
sig_buffer[sig_count++] = '0' + (char)(sig_value % 10);
sig_value /= 10;
}
// Emit significant digits in forward order.
for (int i = sig_count - 1; i >= 0; i--) {
buffer[position++] = sig_buffer[i];
}
// Pad remaining digits with zeros.
int zero_pad_count = (exponent + 1) - sig_count;
for (int i = 0; i < zero_pad_count; i++) {
buffer[position++] = '0';
}
}
// Decimal point and fractional digits.
if (precision > 0 || force_decimal_point) {
buffer[position++] = '.';
}
if (precision > 0) {
// Format fractional digits with leading zeros. The extended precision path
// can push effective_precision up to IREE_PRINTF_MAX_FIXED_FRACTION_DIGITS
// for small fractional values, so the buffer must accommodate that.
char frac_buffer[IREE_PRINTF_MAX_FIXED_FRACTION_DIGITS + 1];
int frac_count = 0;
uint64_t frac_remaining = frac_digits;
for (int i = 0; i < effective_precision; i++) {
frac_buffer[frac_count++] = '0' + (char)(frac_remaining % 10);
frac_remaining /= 10;
}
// Emit fractional digits in forward order (they're stored in reverse).
for (int i = frac_count - 1; i >= 0; i--) {
buffer[position++] = frac_buffer[i];
}
// Track trailing zeros needed beyond our computational limit. These are
// NOT written to the buffer — the caller emits them directly to the
// output to avoid overflowing the fixed-size stack buffer.
if (precision > effective_precision) {
*out_trailing_zeros = precision - effective_precision;
}
}
return position;
}
// Format a non-negative, finite double in %e style (exponential notation).
// Writes into |buffer| and returns the number of characters written.
//
// Trailing fractional zeros beyond the computational precision limit are NOT
// written to the buffer; their count is stored in |*out_trailing_zeros|.
// The caller must emit: buffer[0..mantissa_end], trailing zeros,
// buffer[mantissa_end..end] (the exponent suffix). The split point is stored
// in |*out_exponent_offset|.
//
// Approach: compute all significant digits at once as a single integer via a
// single division, rather than normalizing to [1,10) and routing through the
// fixed-point formatter. The normalize → modf → multiply chain amplifies
// representation error at rounding boundaries (e.g., 575537150/1e8 produces
// 5.75537149999... instead of 5.7553715, flipping a banker's rounding
// decision). By dividing by 10^(exponent - precision) instead, we keep the
// full precision of the original value in a single operation.
static int iree_printf_format_exponential(
char* buffer, double value, int precision, bool force_decimal_point,
bool uppercase, int* out_trailing_zeros, int* out_exponent_offset) {
*out_trailing_zeros = 0;
int position = 0;
if (value == 0.0) {
// Zero: 0.000...e+00.
buffer[position++] = '0';
if (precision > 0 || force_decimal_point) {
buffer[position++] = '.';
// Write zeros up to our buffer limit; track the rest as trailing.
int zeros_to_write = precision;
if (zeros_to_write > IREE_PRINTF_MAX_FLOAT_PRECISION) {
*out_trailing_zeros = zeros_to_write - IREE_PRINTF_MAX_FLOAT_PRECISION;
zeros_to_write = IREE_PRINTF_MAX_FLOAT_PRECISION;
}
for (int i = 0; i < zeros_to_write; i++) {
buffer[position++] = '0';
}
}
*out_exponent_offset = position;
buffer[position++] = uppercase ? 'E' : 'e';
buffer[position++] = '+';
buffer[position++] = '0';
buffer[position++] = '0';
return position;
}
// Compute the base-10 exponent.
int exponent = iree_printf_log10_approx(value);
// We need (precision + 1) significant digits total: 1 before the decimal
// point and |precision| after. Clamp to our computational limit.
int sig_count = precision + 1;
int effective_sig_count = sig_count;
if (effective_sig_count > IREE_PRINTF_MAX_FLOAT_PRECISION) {
effective_sig_count = IREE_PRINTF_MAX_FLOAT_PRECISION;
}
// Scale the value so that its significant digits are an integer with
// exactly |effective_sig_count| digits: scaled ≈ value * 10^n where n is
// chosen to shift the significant digits above the decimal point.
//
// For normal doubles (exponents roughly -290 to +290), we do this in a
// single division/multiplication by 10^|scale_exponent|, which preserves
// full precision. For extreme exponents (subnormals, huge values near
// DBL_MAX), the scale factor itself would overflow double range, so we
// normalize in steps: first bring the value to [1, 10), then scale the
// normalized value by 10^(sig_count - 1).
uint64_t sig_max = (uint64_t)iree_printf_pow10(effective_sig_count);
uint64_t sig_min = sig_max / 10;
int scale_exponent = exponent - effective_sig_count + 1;
double scaled = 0.0;
if (scale_exponent > 290 || scale_exponent < -290) {
// Extreme exponent: normalize to [1, 10) in steps of at most 10^22
// (the largest power of 10 exactly representable as a double), then
// extract digits from the normalized value. This is slightly less
// precise than the single-step approach but doubles only have ~15.9
// decimal digits of precision anyway — the last 1-2 digits of a
// subnormal or extreme value are noise regardless.
double normalized = value;
int remaining = exponent;
if (remaining > 0) {
while (remaining > 22) {
normalized /= 1e22;
remaining -= 22;
}
normalized /= iree_printf_pow10(remaining);
} else if (remaining < 0) {
remaining = -remaining;
while (remaining > 22) {
normalized *= 1e22;
remaining -= 22;
}
normalized *= iree_printf_pow10(remaining);
}
// Correct for approximation errors.
while (normalized >= 10.0) {
normalized /= 10.0;
exponent++;
}
while (normalized < 1.0 && normalized > 0.0) {
normalized *= 10.0;
exponent--;
}
// Now normalized is in [1, 10). Scale to get sig_count digits.
scaled = normalized * iree_printf_pow10(effective_sig_count - 1);
} else {
// Normal case: single-step scaling preserves full precision.
if (scale_exponent >= 0) {
scaled = value / iree_printf_pow10(scale_exponent);
} else {
scaled = value * iree_printf_pow10(-scale_exponent);
}
}
// Round to nearest integer with banker's rounding (round-half-to-even).
//
// The naive approach (look at `scaled - floor(scaled)`) is unreliable
// because the division `value / 10^k` introduces up to 0.5 ULP error,
// which can turn a 0.4999... remainder into 0.5000..., flipping the
// rounding direction. This affects values larger than ~2^50 where the
// double division's rounding error is significant at the unit level.
//
// For the normal-range case (where we divided by 10^k), compute the exact
// remainder via double-double arithmetic: verify the truncated quotient
// with a double-double product, correct if the division rounded up, then
// use the double-double residual for the rounding decision.
uint64_t sig_integer = 0;
if (scale_exponent > 0 && scale_exponent <= 22) {
// Division path: scaled = value / 10^k where 10^k is exactly representable.
//
// For the truncated quotient (floor), use direct division and then verify
// with a double-double product. IEEE 754 correctly-rounded division can
// round the quotient up past an integer boundary when the true fractional
// part is close to 1.0 (specifically, when frac > 1.0 - 0.5*ULP at the
// quotient's magnitude). When this happens, (uint64_t)(value/divisor)
// gives Q+1 instead of Q. We detect and correct this by computing
// sig_integer * divisor as a double-double and checking if it exceeds
// value. After correction, the double-double residual gives the exact
// remainder for the rounding decision.
double divisor = iree_printf_pow10(scale_exponent);
double quotient = value / divisor;
sig_integer = (uint64_t)quotient;
// Verify the floor: double-double product sig_integer * divisor. If the
// product exceeds value, the division rounded up past the true floor.
double product_high = (double)sig_integer * divisor;
double product_low =
iree_printf_mul_error((double)sig_integer, divisor, product_high);
double residual = (value - product_high) - product_low;
if (residual < 0) {
// Division rounded up: true floor is sig_integer - 1. Adjust the
// remainder algebraically rather than recomputing via double-double
// (recomputing would require (double)sig_integer, which loses precision
// when sig_integer > 2^53).
sig_integer--;
residual += divisor;
}
double half = divisor * 0.5;
if (residual > half) {
sig_integer++;
} else if (residual == half) {
if (sig_integer & 1) sig_integer++; // Banker's rounding.
}
} else if (scale_exponent < 0 && scale_exponent >= -290) {
// Multiplication path: scaled = value * 10^|n|. The multiplication can
// introduce rounding error from two sources: (1) the multiplication itself
// (up to 0.5 ULP), and (2) the pow10 scale factor being inexact for n > 22.
// Use iree_printf_mul_pow10_error to get a double-double result that
// corrects both sources (splitting into exact factors for 22 < n <= 44).
double mul_result = 0.0;
double mul_error =
iree_printf_mul_pow10_error(value, -scale_exponent, &mul_result);
sig_integer = (uint64_t)mul_result;
double sig_remainder = (mul_result - (double)sig_integer) + mul_error;
// The combined FMA error from the two-step multiplication can push the
// true remainder outside [0, 1). A negative remainder means the true
// product is below sig_integer; a remainder >= 1.0 means it is at least
// 1 above sig_integer. Normalize into [0, 1) before rounding.
while (sig_remainder >= 1.0) {
sig_integer++;
sig_remainder -= 1.0;
}
while (sig_remainder < 0.0) {
sig_integer--;
sig_remainder += 1.0;
}
if (sig_remainder > 0.5) {
sig_integer++;
} else if (sig_remainder == 0.5) {
if (sig_integer & 1) sig_integer++;
}
} else {
// Extreme exponents (multi-step normalization path): the value has been
// normalized through multiple divisions/multiplications, so FMA-based
// error correction isn't applicable. Direct rounding is sufficient since
// extreme-exponent values have inherently limited precision.
sig_integer = (uint64_t)scaled;
double sig_remainder = scaled - (double)sig_integer;
if (sig_remainder > 0.5) {
sig_integer++;
} else if (sig_remainder == 0.5) {
if (sig_integer & 1) sig_integer++;
}
}
// Handle exponent off by 1 in either direction. The log10 approximation can
// be off by 1, producing too many or too few digits. In either case,
// recompute with the corrected exponent so that rounding happens at the
// correct precision.
//
// This also handles carry from rounding (9999999.5 → 10000000): recomputing
// with exponent+1 gives the same correct result as truncation would, but
// without losing the rounding decision.
//
// For the recomputation we always use the single-step approach. The
// correction is at most ±1, so the new scale_exponent is within 1 of the
// old one — if the old one was in range, so is the new one. If we used the
// multi-step path (extreme exponent), the correction brings us back to the
// multi-step regime. To keep things simple, use the step-based normalization
// for recomputation when the new scale_exponent is extreme.
if (sig_integer >= sig_max || (sig_integer < sig_min && sig_integer > 0)) {
if (sig_integer >= sig_max) {
exponent++;
} else {
exponent--;
}
scale_exponent = exponent - effective_sig_count + 1;
if (scale_exponent > 290 || scale_exponent < -290) {
// Extreme: re-normalize in steps.
double normalized = value;
int remaining = exponent;
if (remaining > 0) {
while (remaining > 22) {
normalized /= 1e22;
remaining -= 22;
}
normalized /= iree_printf_pow10(remaining);
} else if (remaining < 0) {
remaining = -remaining;
while (remaining > 22) {
normalized *= 1e22;
remaining -= 22;
}
normalized *= iree_printf_pow10(remaining);
}
while (normalized >= 10.0) {
normalized /= 10.0;
exponent++;
}
while (normalized < 1.0 && normalized > 0.0) {
normalized *= 10.0;
exponent--;
}
scaled = normalized * iree_printf_pow10(effective_sig_count - 1);
} else {
if (scale_exponent >= 0) {
scaled = value / iree_printf_pow10(scale_exponent);
} else {
scaled = value * iree_printf_pow10(-scale_exponent);
}
}
sig_integer = (uint64_t)scaled;
// Use precise rounding for the recomputed value (same logic as above).
if (scale_exponent > 0 && scale_exponent <= 22) {
// Division path: direct quotient + double-double remainder rounding.
double divisor = iree_printf_pow10(scale_exponent);
double quotient = value / divisor;
sig_integer = (uint64_t)quotient;
double product_high = (double)sig_integer * divisor;
double product_low =
iree_printf_mul_error((double)sig_integer, divisor, product_high);
double residual = (value - product_high) - product_low;
if (residual < 0) {
// Same algebraic correction as the main division path above.
sig_integer--;
residual += divisor;
}
double half = divisor * 0.5;
if (residual > half) {
sig_integer++;
} else if (residual == half) {
if (sig_integer & 1) sig_integer++;
}
} else if (scale_exponent < 0 && scale_exponent >= -290) {
// Multiplication path recomputation: use two-step FMA (same as above).
double mul_result = 0.0;
double mul_error =
iree_printf_mul_pow10_error(value, -scale_exponent, &mul_result);
sig_integer = (uint64_t)mul_result;
double sig_remainder = (mul_result - (double)sig_integer) + mul_error;
while (sig_remainder >= 1.0) {
sig_integer++;
sig_remainder -= 1.0;
}
while (sig_remainder < 0.0) {
sig_integer--;
sig_remainder += 1.0;
}
if (sig_remainder > 0.5) {
sig_integer++;
} else if (sig_remainder == 0.5) {
if (sig_integer & 1) sig_integer++;
}
} else {
double sig_remainder = scaled - (double)sig_integer;
if (sig_remainder > 0.5) {
sig_integer++;
} else if (sig_remainder == 0.5) {
if (sig_integer & 1) sig_integer++;
}
}
// After correction, carry is still possible (value at exact power-of-10
// boundary). Use a loop rather than a single check: while a single carry
// (sig_integer == sig_max) is the expected case, a loop is defensive
// against hypothetical multi-digit overshoots from compounding FMA errors.
while (sig_integer >= sig_max) {
sig_integer /= 10;
exponent++;
}
}
// Extract digits from the integer in reverse order.
char sig_digits[IREE_PRINTF_MAX_FLOAT_PRECISION + 1];
int digit_count = 0;
uint64_t temp = sig_integer;
while (temp > 0) {
sig_digits[digit_count++] = '0' + (char)(temp % 10);
temp /= 10;
}
// Pad to effective_sig_count if needed (e.g., value was 1.000000).
while (digit_count < effective_sig_count) {
sig_digits[digit_count++] = '0';
}
// Emit the leading digit (most significant).
buffer[position++] = sig_digits[digit_count - 1];
// Decimal point and fractional digits.
if (precision > 0 || force_decimal_point) {
buffer[position++] = '.';
}
// Emit computed significant digits after the decimal point.
int frac_digits_emitted = 0;
for (int i = digit_count - 2; i >= 0 && frac_digits_emitted < precision;
i--, frac_digits_emitted++) {
buffer[position++] = sig_digits[i];
}
// Track trailing zeros needed beyond our computational limit. These are NOT
// written to the buffer — the caller inserts them between the mantissa and
// the exponent suffix to avoid overflowing the fixed-size stack buffer.
if (precision > frac_digits_emitted) {
*out_trailing_zeros = precision - frac_digits_emitted;
}
// Record where the exponent suffix starts so the caller can insert trailing
// zeros before it.
*out_exponent_offset = position;
// Exponent: e+dd (at least 2 digits, 3 for exponents >= 100).
buffer[position++] = uppercase ? 'E' : 'e';
if (exponent >= 0) {
buffer[position++] = '+';
} else {
buffer[position++] = '-';
exponent = -exponent;
}
if (exponent >= 100) {
buffer[position++] = '0' + (char)(exponent / 100);
buffer[position++] = '0' + (char)((exponent / 10) % 10);
buffer[position++] = '0' + (char)(exponent % 10);
} else {
buffer[position++] = '0' + (char)(exponent / 10);
buffer[position++] = '0' + (char)(exponent % 10);
}
return position;
}
// Entry point for floating-point formatting. Handles sign, special values
// (NaN, Inf), and dispatches to the appropriate formatter.
static void iree_printf_format_float(iree_printf_output_t* out,
const iree_printf_spec_t* spec,
va_list* args) {
double value = va_arg(*args, double);
bool uppercase = (spec->specifier == 'F' || spec->specifier == 'E' ||
spec->specifier == 'G');
// Handle sign.
bool is_negative = iree_printf_double_is_negative(value);
const char* sign_prefix = "";
int sign_length = 0;
if (is_negative) {
sign_prefix = "-";
sign_length = 1;
value = -value; // Work with absolute value.
} else if (spec->flags & IREE_PRINTF_FLAG_PLUS) {
sign_prefix = "+";
sign_length = 1;
} else if (spec->flags & IREE_PRINTF_FLAG_SPACE) {
sign_prefix = " ";
sign_length = 1;
}
// Handle special values: NaN and Inf.
if (iree_printf_double_is_nan(value)) {
const char* text = uppercase ? "NAN" : "nan";
int content_width = sign_length + 3;
int padding = 0;
if (spec->width > content_width) padding = spec->width - content_width;
if (!(spec->flags & IREE_PRINTF_FLAG_LEFT)) {
iree_printf_output_fill(out, ' ', padding);
}
iree_printf_output_string(out, sign_prefix, sign_length);
iree_printf_output_string(out, text, 3);
if (spec->flags & IREE_PRINTF_FLAG_LEFT) {
iree_printf_output_fill(out, ' ', padding);
}
return;
}
if (iree_printf_double_is_inf(value)) {
const char* text = uppercase ? "INF" : "inf";
int content_width = sign_length + 3;
int padding = 0;
if (spec->width > content_width) padding = spec->width - content_width;
if (!(spec->flags & IREE_PRINTF_FLAG_LEFT)) {
iree_printf_output_fill(out, ' ', padding);
}
iree_printf_output_string(out, sign_prefix, sign_length);
iree_printf_output_string(out, text, 3);
if (spec->flags & IREE_PRINTF_FLAG_LEFT) {
iree_printf_output_fill(out, ' ', padding);
}
return;
}
// Default precision is 6.
int precision = spec->has_precision ? spec->precision : 6;
bool force_decimal_point = (spec->flags & IREE_PRINTF_FLAG_HASH) != 0;
// Format into a temporary buffer. The formatters may report trailing zeros
// that should NOT be written to the buffer (to avoid stack overflow with
// large precisions). These are emitted directly to the output.
char buffer[IREE_PRINTF_FLOAT_BUFFER_SIZE];
int length = 0;
int trailing_zeros = 0;
// For %e: the buffer contains [mantissa][exponent_suffix]. Trailing zeros
// must be inserted between the mantissa and exponent parts.
int exponent_offset = 0;
bool has_exponent_suffix = false;
char specifier = spec->specifier;
// Normalize case for dispatch.
if (specifier == 'F') specifier = 'f';
if (specifier == 'E') specifier = 'e';
if (specifier == 'G') specifier = 'g';
if (specifier == 'f') {
length = iree_printf_format_fixed(buffer, value, precision,
force_decimal_point, &trailing_zeros);
} else if (specifier == 'e') {
length = iree_printf_format_exponential(buffer, value, precision,
force_decimal_point, uppercase,
&trailing_zeros, &exponent_offset);
has_exponent_suffix = true;
} else {
// %g: choose between %f and %e based on the exponent.
// Use %e if exponent < -4 or exponent >= precision.
// For %g, precision means "significant digits", not "digits after decimal".
int sig_precision = precision;
if (sig_precision == 0) sig_precision = 1; // %g with precision 0 is 1.
int exponent = 0;
if (value != 0.0) {
exponent = iree_printf_log10_approx(value);
// Correct approximation.
double check = value / iree_printf_pow10(exponent);
if (check >= 10.0) {
exponent++;
} else if (check < 1.0) {
exponent--;
}
// The C standard says the %g routing decision uses the exponent *after*
// rounding to sig_precision significant digits. If rounding carries over
// (e.g., 9.5 rounded to 1 sig digit becomes 10), the exponent increases.
// Use multiplication (not division) with error correction to get the
// exact scaled significand, then check if rounding carries.
int mul_exp = sig_precision - exponent - 1;
if (mul_exp >= 0 && mul_exp <= 22) {
double scale = iree_printf_pow10(mul_exp);
double scaled = value * scale;
double mul_err = iree_printf_mul_error(value, scale, scaled);
double int_part = iree_printf_floor(scaled);
double frac = (scaled - int_part) + mul_err;
if (frac < 0) {
int_part -= 1.0;
frac += 1.0;
}
bool rounds_up =
frac > 0.5 || (frac == 0.5 && iree_printf_is_odd(int_part));
if (rounds_up && (int_part + 1.0) >= iree_printf_pow10(sig_precision)) {
exponent++;
}
} else if (mul_exp < 0 && mul_exp >= -22) {
// Division path: value * 10^(-|mul_exp|) would lose precision, so
// divide instead. Division by exact pow10 (|mul_exp| <= 22) is safe.
// Use double-double product to get exact remainder (avoids precision
// loss when int_part > 2^53).
double divisor = iree_printf_pow10(-mul_exp);
double quotient = value / divisor;
double int_part = iree_printf_floor(quotient);
double product_high = int_part * divisor;
double product_low =
iree_printf_mul_error(int_part, divisor, product_high);
double remainder = (value - product_high) - product_low;
if (remainder < 0) {
int_part -= 1.0;
remainder += divisor;
}
double half = divisor * 0.5;
bool rounds_up = remainder > half ||
(remainder == half && iree_printf_is_odd(int_part));
if (rounds_up && (int_part + 1.0) >= iree_printf_pow10(sig_precision)) {
exponent++;
}
}
}
if (exponent < -4 || exponent >= sig_precision) {
// Use exponential notation. Precision for %e is sig_precision - 1
// (significant digits minus the one before the decimal point).
length = iree_printf_format_exponential(
buffer, value, sig_precision - 1, force_decimal_point, uppercase,
&trailing_zeros, &exponent_offset);
has_exponent_suffix = true;
} else {
// Use fixed notation. Precision for %f is sig_precision - exponent - 1
// (digits after the decimal point to get the right significant digits).
int f_precision = sig_precision - exponent - 1;
if (f_precision < 0) f_precision = 0;
length = iree_printf_format_fixed(buffer, value, f_precision,
force_decimal_point, &trailing_zeros);
}
// Strip trailing zeros after the decimal point (unless '#' flag).
// Any virtual trailing zeros (not in the buffer) are also stripped.
if (!force_decimal_point) {
trailing_zeros = 0;
// Find the decimal point and exponent marker positions.
int decimal_position = -1;
int exponent_position = -1;
for (int i = 0; i < length; i++) {
if (buffer[i] == '.') decimal_position = i;
if (buffer[i] == 'e' || buffer[i] == 'E') {
exponent_position = i;
break;
}
}
if (decimal_position >= 0) {
int strip_end = (exponent_position >= 0) ? exponent_position : length;
while (strip_end > decimal_position + 1 &&
buffer[strip_end - 1] == '0') {
strip_end--;
}
// Also strip the decimal point if all fractional digits were removed.
if (strip_end == decimal_position + 1) {
strip_end = decimal_position;
}
if (exponent_position >= 0) {
// Move the exponent part down.
int exponent_part_length = length - exponent_position;
memmove(buffer + strip_end, buffer + exponent_position,
exponent_part_length);
length = strip_end + exponent_part_length;
exponent_offset = strip_end;
} else {
length = strip_end;
}
}
}
}
// Now emit with sign and padding.
// Total content width includes the buffer contents plus any trailing zeros
// that aren't in the buffer.
int content_width = sign_length + length + trailing_zeros;
int padding = 0;
if (spec->width > content_width) {
padding = spec->width - content_width;
}
bool use_zero_pad = (spec->flags & IREE_PRINTF_FLAG_ZERO) &&
!(spec->flags & IREE_PRINTF_FLAG_LEFT);
if (use_zero_pad) {
iree_printf_output_string(out, sign_prefix, sign_length);
iree_printf_output_fill(out, '0', padding);
} else if (!(spec->flags & IREE_PRINTF_FLAG_LEFT)) {
iree_printf_output_fill(out, ' ', padding);
iree_printf_output_string(out, sign_prefix, sign_length);
} else {
iree_printf_output_string(out, sign_prefix, sign_length);
}
// Emit the buffer contents with trailing zeros in the right position.
// For %e/%E: trailing zeros go between the mantissa and the exponent suffix.
// For %f/%F: trailing zeros go at the end.
if (has_exponent_suffix && trailing_zeros > 0) {
iree_printf_output_string(out, buffer, exponent_offset);
iree_printf_output_fill(out, '0', trailing_zeros);
iree_printf_output_string(out, buffer + exponent_offset,
length - exponent_offset);
} else {
iree_printf_output_string(out, buffer, length);
iree_printf_output_fill(out, '0', trailing_zeros);
}
if (spec->flags & IREE_PRINTF_FLAG_LEFT) {
iree_printf_output_fill(out, ' ', padding);
}
}
//===----------------------------------------------------------------------===//
// Main format dispatcher
//===----------------------------------------------------------------------===//
// Format a string according to |format| with arguments from |args|.
// Writes output through |out|. Returns the total number of characters that
// would be written (excluding NUL), or -1 on format error.
static int iree_printf_format(iree_printf_output_t* out, const char* format,
va_list args) {
// We need a mutable copy of the va_list because the spec parser consumes
// arguments (for '*' width/precision).
va_list args_copy;
va_copy(args_copy, args);
const char* p = format;
while (*p) {
if (*p != '%') {
// Literal character — emit directly.
iree_printf_output_char(out, *p);
p++;
continue;
}
p++; // Skip '%'.
// '%%' → literal '%'.
if (*p == '%') {
iree_printf_output_char(out, '%');
p++;
continue;
}
// Parse the format specifier.
iree_printf_spec_t spec = {0};
const char* next = iree_printf_parse_spec(p, &args_copy, &spec);
if (!next) {
// Truncated or malformed format string.
va_end(args_copy);
return -1;
}
p = next;
// Dispatch to the appropriate formatter.
switch (spec.specifier) {
case 'd':
case 'i':
case 'u':
case 'o':
case 'x':
case 'X':
iree_printf_format_integer(out, &spec, &args_copy);
break;
case 's':
iree_printf_format_string(out, &spec, &args_copy);
break;
case 'c':
iree_printf_format_char(out, &spec, &args_copy);
break;
case 'p':
iree_printf_format_pointer(out, &spec, &args_copy);
break;
case 'f':
case 'F':
case 'e':
case 'E':
case 'g':
case 'G':
iree_printf_format_float(out, &spec, &args_copy);
break;
default:
// Unknown specifier. Fail loudly — do not silently skip.
va_end(args_copy);
return -1;
}
}
va_end(args_copy);
// Guard against size_t→int overflow. Formatted output exceeding INT_MAX bytes
// is not representable as a return value; return -1 to signal the error
// rather than wrapping to a bogus (possibly negative) count that callers
// might use to size subsequent allocations.
if (out->position > (size_t)INT_MAX) return -1;
return (int)out->position;
}
//===----------------------------------------------------------------------===//
// Public API implementation
//===----------------------------------------------------------------------===//
int iree_vsnprintf(char* buffer, size_t count, const char* format,
va_list varargs) {
iree_printf_output_t out = {
.buffer = buffer,
.capacity = count > 0 ? count - 1 : 0, // Reserve 1 byte for NUL.
.position = 0,
.callback = NULL,
.callback_data = NULL,
};
int result = iree_printf_format(&out, format, varargs);
// NUL-terminate the buffer. If buffer is NULL (dry-run), skip.
if (buffer) {
if (count > 0) {
size_t nul_position = out.position < count - 1 ? out.position : count - 1;
buffer[nul_position] = '\0';
}
}
return result;
}
int iree_snprintf(char* buffer, size_t count, const char* format, ...) {
va_list varargs;
va_start(varargs, format);
int result = iree_vsnprintf(buffer, count, format, varargs);
va_end(varargs);
return result;
}
int iree_vfctprintf(iree_printf_callback_t callback, void* user_data,
const char* format, va_list varargs) {
iree_printf_output_t out = {
.buffer = NULL,
.capacity = 0,
.position = 0,
.callback = callback,
.callback_data = user_data,
};
return iree_printf_format(&out, format, varargs);
}
int iree_fctprintf(iree_printf_callback_t callback, void* user_data,
const char* format, ...) {
va_list varargs;
va_start(varargs, format);
int result = iree_vfctprintf(callback, user_data, format, varargs);
va_end(varargs);
return result;
}