Skip site navigation (1)Skip section navigation (2)
Date:      Sun, 02 Aug 2026 12:10:52 +0000
From:      Robert Clausecker <fuz@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Cc:        Faraz Vahedi <kfv@kfv.io>
Subject:   git: f68d7bfc1479 - main - libc: Add strfromd, strfromf, and strfroml per C23
Message-ID:  <6a6f33cc.1cf60.548886f9@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by fuz:

URL: https://cgit.FreeBSD.org/src/commit/?id=f68d7bfc1479042184e09431bd55771c50c47f68

commit f68d7bfc1479042184e09431bd55771c50c47f68
Author:     Faraz Vahedi <kfv@kfv.io>
AuthorDate: 2026-06-27 13:34:21 +0000
Commit:     Robert Clausecker <fuz@FreeBSD.org>
CommitDate: 2026-08-02 12:10:02 +0000

    libc: Add strfromd, strfromf, and strfroml per C23
    
    strfromd(), strfromf(), and strfroml() are implemented directly
    in terms of gdtoa.  If a non-conforming format string is passed,
    the string "EDOOFUS" is returned and errno set to EDOOFUS as an
    extension.
    
    Reviewed by:    fuz
    MFC after:      1 month
    Pull-Request:   https://github.com/freebsd/freebsd-src/pull/2301
    Signed-off-by:  Faraz Vahedi <kfv@kfv.io>
---
 include/stdlib.h                     |   4 +
 lib/libc/stdlib/Makefile.inc         |   8 +-
 lib/libc/stdlib/Symbol.map           |   3 +
 lib/libc/stdlib/strfrom.c            | 476 ++++++++++++++++++++++++++++
 lib/libc/stdlib/strfrom.h            |  35 +++
 lib/libc/stdlib/strfromd.3           | 109 +++++++
 lib/libc/stdlib/strfromd.c           |  45 +++
 lib/libc/stdlib/strfromf.c           |  47 +++
 lib/libc/stdlib/strfroml.c           |  46 +++
 lib/libc/tests/stdlib/Makefile       |   1 +
 lib/libc/tests/stdlib/strfrom_test.c | 590 +++++++++++++++++++++++++++++++++++
 11 files changed, 1363 insertions(+), 1 deletion(-)

diff --git a/include/stdlib.h b/include/stdlib.h
index 82a347f5317e..c1f465e21fd1 100644
--- a/include/stdlib.h
+++ b/include/stdlib.h
@@ -175,6 +175,10 @@ _Noreturn void
 size_t	memalignment(const void *) __pure2;
 void	free_sized(void *, size_t) __noexcept;
 void	free_aligned_sized(void *, size_t, size_t) __noexcept;
+int	strfromd(char * __restrict, size_t, const char * __restrict, double);
+int	strfromf(char * __restrict, size_t, const char * __restrict, float);
+int	strfroml(char * __restrict, size_t,
+	    const char * __restrict, long double);
 #endif /* __ISO_C_VISIBLE >= 2023 */
 
 /*
diff --git a/lib/libc/stdlib/Makefile.inc b/lib/libc/stdlib/Makefile.inc
index 2cdff5b934cb..7ff6a6c46ea5 100644
--- a/lib/libc/stdlib/Makefile.inc
+++ b/lib/libc/stdlib/Makefile.inc
@@ -55,6 +55,10 @@ MISRCS+= \
 	remque.c \
 	set_constraint_handler_s.c \
 	strfmon.c \
+	strfrom.c \
+	strfromd.c \
+	strfromf.c \
+	strfroml.c \
 	strtoimax.c \
 	strtol.c \
 	strtold.c \
@@ -96,7 +100,7 @@ MAN+=	a64l.3 abort.3 abs.3 atexit.3 atof.3 \
 	quick_exit.3 \
 	radixsort.3 rand.3 random.3 reallocarray.3 reallocf.3 realpath.3 \
 	set_constraint_handler_s.3 \
-	strfmon.3 strtod.3 strtol.3 strtonum.3 strtoul.3 system.3 \
+	strfmon.3 strfromd.3 strtod.3 strtol.3 strtonum.3 strtoul.3 system.3 \
 	tsearch.3
 
 MLINKS+=a64l.3 l64a.3 \
@@ -143,6 +147,8 @@ MLINKS+=reallocarray.3 recallocarray.3
 MLINKS+=set_constraint_handler_s.3 abort_handler_s.3
 MLINKS+=set_constraint_handler_s.3 ignore_handler_s.3
 MLINKS+=strfmon.3 strfmon_l.3
+MLINKS+=strfromd.3 strfromf.3 \
+	strfromd.3 strfroml.3
 MLINKS+=strtod.3 strtof.3 \
 	strtod.3 strtold.3
 MLINKS+=strtol.3 strtoll.3 \
diff --git a/lib/libc/stdlib/Symbol.map b/lib/libc/stdlib/Symbol.map
index 373006b4a388..df4922f1df60 100644
--- a/lib/libc/stdlib/Symbol.map
+++ b/lib/libc/stdlib/Symbol.map
@@ -134,6 +134,9 @@ FBSD_1.8 {
 FBSD_1.9 {
 	memalignment;
 	recallocarray;
+	strfromd;
+	strfromf;
+	strfroml;
 	strtonumx;
 	tdestroy;
 };
diff --git a/lib/libc/stdlib/strfrom.c b/lib/libc/stdlib/strfrom.c
new file mode 100644
index 000000000000..01725ba30d1f
--- /dev/null
+++ b/lib/libc/stdlib/strfrom.c
@@ -0,0 +1,476 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Shared helpers for strfromd, strfromf, and strfroml (C23 §7.24.1.3).
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <locale.h>
+#include <stdlib.h>
+
+#include "strfrom.h"
+
+struct sf_buf {
+	char	*s;
+	size_t	 n;
+	int	 pos;
+};
+
+/*
+ * Write c to b->s[b->pos] if within the n-1 writable bytes; always advance
+ * b->pos.  n is the total buffer capacity, including the null slot.
+ */
+static void
+sf_putc(struct sf_buf *b, char c)
+{
+	if (b->n > 0 && (size_t)b->pos < b->n - 1)
+		b->s[b->pos] = c;
+	b->pos++;
+}
+
+/*
+ * Write len bytes from src.
+ */
+static void
+sf_write(struct sf_buf *b, const char *src, int len)
+{
+	int i;
+
+	for (i = 0; i < len; i++)
+		sf_putc(b, src[i]);
+}
+
+/*
+ * Write a null terminated string.
+ */
+static void
+sf_puts(struct sf_buf *b, const char *src)
+{
+	while (*src != '\0')
+		sf_putc(b, *src++);
+}
+
+/*
+ * Write count copies of c.
+ */
+static void
+sf_padc(struct sf_buf *b, char c, int count)
+{
+	int i;
+
+	for (i = 0; i < count; i++)
+		sf_putc(b, c);
+}
+
+/*
+ * Seal the buffer: null terminate at min(pos, n-1).
+ */
+static void
+sf_seal(struct sf_buf *b)
+{
+	if (b->n > 0)
+		b->s[(size_t)b->pos < b->n ? b->pos : b->n - 1] = '\0';
+}
+
+/*
+ * Emit the radix.  Interfaces are defined as snprintf(3) (C23 §7.24.1.3p2),
+ * whose '.' is the LC_NUMERIC decimal point; localeconv() resolves the same
+ * per-thread locale as vfprintf(3).  The radix may be multibyte.
+ */
+static void
+sf_putdp(struct sf_buf *b)
+{
+	sf_puts(b, localeconv()->decimal_point);
+}
+
+/*
+ * Emit "[eEpP][+-]ddd": exponent sign and decimal magnitude, zero-padded to at
+ * least mindig digits (2 for %e/%E and %g/%G, 1 for %a/%A per C23 §7.23.6.1).
+ *
+ * Handles the full exponent range of every supported type.
+ */
+static void
+sf_emit_exp(struct sf_buf *b, char ec, int exp, int mindig)
+{
+	char buf[16];
+	int len;
+
+	sf_putc(b, ec);
+	if (exp < 0) {
+		sf_putc(b, '-');
+		exp = -exp;
+	} else {
+		sf_putc(b, '+');
+	}
+
+	len = 0;
+	do {
+		buf[len++] = '0' + exp % 10;
+		exp /= 10;
+	} while (exp != 0);
+	while (len < mindig)
+		buf[len++] = '0';
+	while (len > 0)
+		sf_putc(b, buf[--len]);
+}
+
+/*
+ * Parse "%[.prec]conv" per C23 §7.24.1.3.
+ *
+ * Returns the conversion specifier character, or '\0' with errno set to
+ * EDOOFUS if fmt is not one of the forms the standard permits; *prec is
+ * -1 when the precision is absent.
+ */
+char
+__sf_parse_fmt(const char *fmt, int *prec)
+{
+	const char *p;
+
+	*prec = -1;
+	if (*fmt != '%')
+		return ('\0');
+	p = fmt + 1;
+
+	if (*p == '.') {
+		*prec = 0;
+		while (*++p >= '0' && *p <= '9')
+			*prec = *prec * 10 + (*p - '0');
+	}
+
+	switch (*p) {
+	case 'a': case 'A':
+	case 'e': case 'E':
+	case 'f': case 'F':
+	case 'g': case 'G':
+		break;
+	default:
+		return ('\0');
+	}
+
+	return (p[1] == '\0' ? *p : '\0');
+}
+
+/*
+ * Report a format string the standard does not permit.
+ *
+ * Sets errno and renders "EDOOFUS" under the ordinary snprintf(3) truncation
+ * rules, which keeps a faulty caller alive and its output obviously wrong.
+ */
+int
+__sf_edoofus(char *s, size_t n)
+{
+	struct sf_buf b;
+
+	errno = EDOOFUS;
+	b.s = s;
+	b.n = n;
+	b.pos = 0;
+	sf_puts(&b, "EDOOFUS");
+	sf_seal(&b);
+	return (b.pos);
+}
+
+/*
+ * Render Inf or NaN into b->s[0..n-1].
+ *
+ * is_nan: non-zero if digits[0] == 'N' (NaN), zero for Infinity.
+ */
+static int
+sf_special(struct sf_buf *b, char conv, int signflag, int is_nan)
+{
+	int upper;
+
+	b->pos = 0;
+	upper = isupper((unsigned char)conv);
+	if (!is_nan && signflag)
+		sf_putc(b, '-');
+	sf_puts(b, is_nan ? (upper ? "NAN" : "nan") : (upper ? "INF" : "inf"));
+	sf_seal(b);
+	return (b->pos);
+}
+
+/*
+ * Render %e / %E.
+ *
+ * dtoa was called with mode=2, ndigits=prec+1.  decpt is the position of the
+ * first significant digit relative to the decimal point (= exponent + 1), as
+ * returned by dtoa.
+ */
+static int
+sf_efmt(struct sf_buf *b, int prec, char conv,
+    const char *digits, int ndig, int decpt, int signflag)
+{
+	int avail, copy;
+
+	b->pos = 0;
+	if (signflag)
+		sf_putc(b, '-');
+
+	/*
+	 * Leading significant digit.
+	 */
+	sf_putc(b, ndig > 0 ? digits[0] : '0');
+
+	if (prec > 0) {
+		sf_putdp(b);
+		avail = ndig > 1 ? ndig - 1 : 0;
+		copy = avail < prec ? avail : prec;
+		sf_write(b, digits + 1, copy);
+		sf_padc(b, '0', prec - copy);
+	}
+	sf_emit_exp(b, isupper((unsigned char)conv) ? 'E' : 'e', decpt - 1, 2);
+
+	sf_seal(b);
+	return (b->pos);
+}
+
+/*
+ * Render %f / %F.
+ *
+ * dtoa was called with mode=3, ndigits=prec.  decpt gives the number of digits
+ * before the decimal point (may be <= 0).
+ */
+static int
+sf_ffmt(struct sf_buf *b, int prec, int signflag,
+    const char *digits, int ndig, int decpt)
+{
+	int avail, copy, rem, zc;
+
+	b->pos = 0;
+	if (signflag)
+		sf_putc(b, '-');
+
+	if (decpt <= 0) {
+		sf_putc(b, '0');
+		if (prec > 0) {
+			sf_putdp(b);
+			zc = -decpt < prec ? -decpt : prec;
+			rem = prec - zc;
+			copy = ndig < rem ? ndig : rem;
+			sf_padc(b, '0', zc);
+			sf_write(b, digits, copy);
+			sf_padc(b, '0', rem - copy);
+		}
+	} else {
+		/*
+		 * decpt digits (or zeros) before the point.
+		 */
+		copy = ndig < decpt ? ndig : decpt;
+		sf_write(b, digits, copy);
+		sf_padc(b, '0', decpt - copy);
+		if (prec > 0) {
+			sf_putdp(b);
+			avail = ndig - decpt;
+			if (avail < 0)
+				avail = 0;
+			copy = avail < prec ? avail : prec;
+			sf_write(b, digits + decpt, copy);
+			sf_padc(b, '0', prec - copy);
+		}
+	}
+
+	sf_seal(b);
+	return (b->pos);
+}
+
+/*
+ * Render %g / %G.
+ *
+ * dtoa was called with mode=2, ndigits=max(1, prec).
+ */
+static int
+sf_gfmt(struct sf_buf *b, int prec, char conv,
+    const char *digits, int ndig, int decpt, int signflag)
+{
+	int ep, copy, frac;
+
+	b->pos = 0;
+	/*
+	 * Precision 0 is treated as 1 per C23 §7.23.6.1.
+	 */
+	ep = prec == 0 ? 1 : prec;
+
+	/*
+	 * Strip trailing zeros (no ALT flag is accepted).
+	 */
+	while (ndig > 1 && digits[ndig - 1] == '0')
+		ndig--;
+
+	if (signflag)
+		sf_putc(b, '-');
+
+	/*
+	 * Standard says −4 ≤ exponent < P test: exponent = decpt − 1
+	 */
+	if (decpt > -4 && decpt <= ep) {
+		/*
+		 * %f style, per C23 §7.23.6.1.
+		 */
+		if (decpt <= 0) {
+			sf_putc(b, '0');
+			if (ndig > 0) {
+				sf_putdp(b);
+				sf_padc(b, '0', -decpt);
+				sf_write(b, digits, ndig);
+			}
+		} else {
+			copy = ndig < decpt ? ndig : decpt;
+			sf_write(b, digits, copy);
+			sf_padc(b, '0', decpt - copy);
+			frac = ndig - decpt;
+			if (frac > 0) {
+				sf_putdp(b);
+				sf_write(b, digits + decpt, frac);
+			}
+		}
+	} else {
+		/*
+		 * %e style, per C23 §7.23.6.1.
+		 */
+		sf_putc(b, ndig > 0 ? digits[0] : '0');
+		if (ndig > 1) {
+			sf_putdp(b);
+			sf_write(b, digits + 1, ndig - 1);
+		}
+		sf_emit_exp(b, isupper((unsigned char)conv) ? 'E' : 'e',
+		    decpt - 1, 2);
+	}
+
+	sf_seal(b);
+	return (b->pos);
+}
+
+/*
+ * Render %a / %A.
+ *
+ * digits / ndig: hex significand digits from __hdtoa / __hldtoa; the first
+ * digit represents the integer part of the mantissa (normally '1').
+ *
+ * decpt: binary exponent such that p-exponent = decpt - 1.
+ *
+ * user_prec: digits after the hex point (-1 for shortest-exact).
+ */
+static int
+sf_afmt(struct sf_buf *b, int user_prec, char conv,
+    const char *digits, int ndig, int decpt, int signflag)
+{
+	int after, copy;
+
+	b->pos = 0;
+	if (signflag)
+		sf_putc(b, '-');
+	sf_putc(b, '0');
+	sf_putc(b, isupper((unsigned char)conv) ? 'X' : 'x');
+
+	/*
+	 * Integer part of the significand.
+	 */
+	sf_putc(b, ndig > 0 ? digits[0] : '0');
+
+	after = ndig - 1;
+	if (user_prec < 0) {
+		/*
+		 * Shortest-exact: emit only the non-zero significant tail.
+		 */
+		if (after > 0) {
+			sf_putdp(b);
+			sf_write(b, digits + 1, after);
+		}
+	} else if (user_prec > 0) {
+		sf_putdp(b);
+		copy = after < user_prec ? after : user_prec;
+		sf_write(b, digits + 1, copy);
+		sf_padc(b, '0', user_prec - copy);
+	}
+	sf_emit_exp(b, isupper((unsigned char)conv) ? 'P' : 'p', decpt - 1, 1);
+
+	sf_seal(b);
+	return (b->pos);
+}
+
+/*
+ * Hex digit table for %a / %A, selected by specifier case.
+ */
+const char *
+__sf_xdigits(char conv)
+{
+	return (isupper((unsigned char)conv) ? "0123456789ABCDEF" :
+	    "0123456789abcdef");
+}
+
+/*
+ * Map a decimal specifier (lowercased) to its gdtoa mode and digit count.
+ */
+void
+__sf_decimal_mode(char lc, int prec, int *mode, int *ndig_req)
+{
+	switch (lc) {
+	case 'e':
+		*mode = 2;
+		*ndig_req = prec + 1;
+		break;
+	case 'f':
+		*mode = 3;
+		*ndig_req = prec;
+		break;
+	case 'g':
+		*mode = 2;
+		*ndig_req = prec == 0 ? 1 : prec;
+		break;
+	default:
+		__unreachable();
+	}
+}
+
+/*
+ * Render a finished %a / %A conversion.  __hdtoa / __hldtoa always flag
+ * Inf/NaN with decpt == INT_MAX.
+ */
+int
+__sf_render_hex(char *s, size_t n, char conv, int prec,
+    char *digits, char *dend, int decpt, int signflag)
+{
+	struct sf_buf b;
+	int ndig, usprec;
+
+	b.s = s;
+	b.n = n;
+	if (decpt == INT_MAX)
+		return (sf_special(&b, conv, signflag, digits[0] == 'N'));
+	ndig = (int)(dend - digits);
+	usprec = prec >= 0 ? prec : ndig - 1;
+	return (sf_afmt(&b, usprec, conv, digits, ndig, decpt, signflag));
+}
+
+/*
+ * Render a finished %e/%f/%g conversion.  is_special is set when decpt holds
+ * the caller's Inf/NaN sentinel (dtoa: 9999, __ldtoa: INT_MAX).
+ */
+int
+__sf_render_decimal(char *s, size_t n, char conv, char lc, int prec,
+    char *digits, char *dend, int decpt, int signflag, int is_special)
+{
+	struct sf_buf b;
+	int ndig;
+
+	b.s = s;
+	b.n = n;
+	if (is_special)
+		return (sf_special(&b, conv, signflag, digits[0] == 'N'));
+	ndig = (int)(dend - digits);
+	switch (lc) {
+	case 'e':
+		return (sf_efmt(&b, prec, conv, digits, ndig, decpt, signflag));
+	case 'f':
+		return (sf_ffmt(&b, prec, signflag, digits, ndig, decpt));
+	case 'g':
+		return (sf_gfmt(&b, prec, conv, digits, ndig, decpt, signflag));
+	default:
+		__unreachable();
+	}
+}
diff --git a/lib/libc/stdlib/strfrom.h b/lib/libc/stdlib/strfrom.h
new file mode 100644
index 000000000000..967897ea7f12
--- /dev/null
+++ b/lib/libc/stdlib/strfrom.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Internal declarations for strfrom* numeric conversion (C23 §7.24.1.3).
+ *
+ * Note: gdtoa dtoa() modes used:
+ *   2 – ndigits significant digits      (%e, %g)
+ *   3 – ndigits digits after '.'        (%f)
+ */
+
+#ifndef STRFROM_H
+#define	STRFROM_H
+
+#include <limits.h>
+#include <stddef.h>
+
+#define	dtoa		__dtoa
+#define	freedtoa	__freedtoa
+
+#include "../stdio/floatio.h"
+#include "gdtoa.h"
+
+char	__sf_parse_fmt(const char *, int *);
+int	__sf_edoofus(char *, size_t);
+const char *__sf_xdigits(char);
+void	__sf_decimal_mode(char, int, int *, int *);
+int	__sf_render_hex(char *, size_t, char, int, char *, char *, int, int);
+int	__sf_render_decimal(char *, size_t, char, char, int, char *, char *,
+	    int, int, int);
+
+#endif /* STRFROM_H */
diff --git a/lib/libc/stdlib/strfromd.3 b/lib/libc/stdlib/strfromd.3
new file mode 100644
index 000000000000..acd3867c0425
--- /dev/null
+++ b/lib/libc/stdlib/strfromd.3
@@ -0,0 +1,109 @@
+.\"
+.\" Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd June 27, 2026
+.Dt STRFROMD 3
+.Os
+.Sh NAME
+.Nm strfromd ,
+.Nm strfromf ,
+.Nm strfroml
+.Nd convert floating point value to
+.Tn ASCII
+string
+.Sh LIBRARY
+.Lb libc
+.Sh SYNOPSIS
+.In stdlib.h
+.Ft int
+.Fn strfromd "char * restrict str" "size_t size" "const char * restrict format" "double fp"
+.Ft int
+.Fn strfromf "char * restrict str" "size_t size" "const char * restrict format" "float fp"
+.Ft int
+.Fn strfroml "char * restrict str" "size_t size" "const char * restrict format" "long double fp"
+.Sh DESCRIPTION
+Functions
+.Fn strfromd ,
+.Fn strfromf ,
+and
+.Fn strfroml
+convert the floating-point value
+.Fa fp
+to a null terminated string, storing at most
+.Fa size
+bytes
+.Pq including the terminating null character
+into the array pointed to by
+.Fa str .
+.Pp
+These functions are equivalent to
+.Fn snprintf "s" "n" "format" "fp" ,
+except that default argument promotions are not applied to
+.Fa fp ,
+and the
+.Fa format
+string is restricted.
+It shall consist of exactly the character
+.Ql % ,
+followed by an optional precision that does not contain an asterisk
+.Ql * ,
+followed by one of the conversion specifiers
+.Cm a , A , e , E , f , F , g ,
+or
+.Cm G .
+The conversion specifier applies to the type indicated by the
+function suffix, rather than by a length modifier.
+.Pp
+Use of any other format string results in undefined behaviour,
+as does any undefined behaviour inherited from
+.Xr snprintf 3
+per se.
+In case of a format string not specified by the standard, the string
+.Ql EDOOFUS
+is written to
+.Fa str ,
+subject to the same truncation rules as a successful conversion, and
+.Va errno
+is set to
+.Er EDOOFUS .
+.Sh RETURN VALUES
+The
+.Fn strfromd ,
+.Fn strfromf ,
+and
+.Fn strfroml
+functions, as per
+.Xr snprintf 3 ,
+return the number of characters
+.Po
+not counting the terminating null character
+.Pc
+that would have been written
+had
+.Fa n
+been sufficiently large.
+Thus, the null terminated output has been completely written if
+and only if the returned value is both nonnegative and less than
+.Fa size .
+.Sh ERRORS
+.Bl -tag -width Er
+.It Bq Er EDOOFUS
+The
+.Fa format
+string is not of the form the standard specifies.
+.El
+.Sh SEE ALSO
+.Xr snprintf 3 ,
+.Xr strtod 3 ,
+.Xr strtol 3 ,
+.Xr strtoul 3
+.Sh STANDARDS
+Functions
+.Fn strfromd ,
+.Fn strfromf ,
+and
+.Fn strfroml
+conform to
+.St -isoC-2023 .
diff --git a/lib/libc/stdlib/strfromd.c b/lib/libc/stdlib/strfromd.c
new file mode 100644
index 000000000000..b7d364c0d9cd
--- /dev/null
+++ b/lib/libc/stdlib/strfromd.c
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <stdlib.h>
+
+#include "strfrom.h"
+
+int
+strfromd(char * __restrict s, size_t n, const char * __restrict fmt, double fp)
+{
+	char conv, lc, *digits, *dend;
+	int prec, decpt, signflag, ret, mode, ndig_req;
+
+	conv = __sf_parse_fmt(fmt, &prec);
+	if (conv == '\0')
+		return (__sf_edoofus(s, n));
+	lc = tolower((unsigned char)conv);
+
+	if (lc == 'a') {
+		digits = __hdtoa(fp, __sf_xdigits(conv),
+		    prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend);
+		ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt,
+		    signflag);
+		freedtoa(digits);
+		return (ret);
+	}
+
+	if (prec < 0)
+		prec = 6;
+
+	__sf_decimal_mode(lc, prec, &mode, &ndig_req);
+
+	digits = dtoa(fp, mode, ndig_req, &decpt, &signflag, &dend);
+
+	ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt,
+	    signflag, decpt == 9999);
+
+	freedtoa(digits);
+
+	return (ret);
+}
diff --git a/lib/libc/stdlib/strfromf.c b/lib/libc/stdlib/strfromf.c
new file mode 100644
index 000000000000..adb87cf2fdda
--- /dev/null
+++ b/lib/libc/stdlib/strfromf.c
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <stdlib.h>
+
+#include "strfrom.h"
+
+int
+strfromf(char * __restrict s, size_t n, const char * __restrict fmt, float fp)
+{
+	char conv, lc, *digits, *dend;
+	int prec, decpt, signflag, ret, mode, ndig_req;
+	double dbl;
+
+	dbl = (double)fp;
+	conv = __sf_parse_fmt(fmt, &prec);
+	if (conv == '\0')
+		return (__sf_edoofus(s, n));
+	lc = tolower((unsigned char)conv);
+
+	if (lc == 'a') {
+		digits = __hdtoa(dbl, __sf_xdigits(conv),
+		    prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend);
+		ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt,
+		    signflag);
+		freedtoa(digits);
+		return (ret);
+	}
+
+	if (prec < 0)
+		prec = 6;
+
+	__sf_decimal_mode(lc, prec, &mode, &ndig_req);
+
+	digits = dtoa(dbl, mode, ndig_req, &decpt, &signflag, &dend);
+
+	ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt,
+	    signflag, decpt == 9999);
+
+	freedtoa(digits);
+
+	return (ret);
+}
diff --git a/lib/libc/stdlib/strfroml.c b/lib/libc/stdlib/strfroml.c
new file mode 100644
index 000000000000..9e213a5ecbee
--- /dev/null
+++ b/lib/libc/stdlib/strfroml.c
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <stdlib.h>
+
+#include "strfrom.h"
+
+int
+strfroml(char * __restrict s, size_t n, const char * __restrict fmt,
+    long double fp)
+{
+	char conv, lc, *digits, *dend;
+	int prec, decpt, signflag, ret, mode, ndig_req;
+
+	conv = __sf_parse_fmt(fmt, &prec);
+	if (conv == '\0')
+		return (__sf_edoofus(s, n));
+	lc = tolower((unsigned char)conv);
+
+	if (lc == 'a') {
+		digits = __hldtoa(fp, __sf_xdigits(conv),
+		    prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend);
+		ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt,
+		    signflag);
+		freedtoa(digits);
+		return (ret);
+	}
+
+	if (prec < 0)
+		prec = 6;
+
+	__sf_decimal_mode(lc, prec, &mode, &ndig_req);
+
+	digits = __ldtoa(&fp, mode, ndig_req, &decpt, &signflag, &dend);
+
+	ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt,
+	    signflag, decpt == INT_MAX);
+
+	freedtoa(digits);
+
+	return (ret);
+}
diff --git a/lib/libc/tests/stdlib/Makefile b/lib/libc/tests/stdlib/Makefile
index 0dbe059e5264..1a0206bcdab9 100644
--- a/lib/libc/tests/stdlib/Makefile
+++ b/lib/libc/tests/stdlib/Makefile
@@ -17,6 +17,7 @@ ATF_TESTS_C+=		qsort_s_test
 ATF_TESTS_C+=		qsort_bench
 ATF_TESTS_C+=		set_constraint_handler_s_test
 ATF_TESTS_C+=		strfmon_test
+ATF_TESTS_C+=		strfrom_test
 ATF_TESTS_C+=		system_test
 ATF_TESTS_C+=		tsearch_test
 ATF_TESTS_CXX+=		cxa_thread_atexit_test
diff --git a/lib/libc/tests/stdlib/strfrom_test.c b/lib/libc/tests/stdlib/strfrom_test.c
new file mode 100644
index 000000000000..8898cde200a6
--- /dev/null
+++ b/lib/libc/tests/stdlib/strfrom_test.c
@@ -0,0 +1,590 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Tests for strfromd(3), strfromf(3), and strfroml(3).
+ *
+ * C23 §7.24.1.3 states that these functions are equivalent to snprintf(3),
+ * albeit with some subtle exceptions, and therefore snprintf(3) is used as
+ * the correctness oracle.  Hardcoded string checks guard against both
+ * producing the same wrong answer.
+ */
+
+#include <errno.h>
+#include <float.h>
+#include <locale.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <atf-c.h>
+
+/*
+ * Buffer large enough for any finite value in %f form, including LDBL_MAX on
+ * platforms with 80-bit or 128-bit long double.  Sizing for the full output
+ * keeps the snprintf(3) oracle a full-length comparison rather than letting
+ * both sides truncate alike.
+ */
+#define	FBUF	8192
+
+static void
+check_d(const char *fmt, double val)
+{
+	char got[FBUF], ref[FBUF];
+	int rlen, glen;
+
+	rlen = snprintf(ref, sizeof(ref), fmt, val);
+	glen = strfromd(got, sizeof(got), fmt, val);
+	ATF_CHECK_MSG(glen == rlen,
+	    "strfromd(\"%s\", %g): return %d, want %d", fmt, val, glen, rlen);
+	ATF_CHECK_STREQ_MSG(ref, got, "strfromd(\"%s\", %g)", fmt, val);
+}
+
+static void
+check_f(const char *fmt, float val)
+{
+	char got[FBUF], ref[FBUF];
+	int rlen, glen;
+
+	rlen = snprintf(ref, sizeof(ref), fmt, (double)val);
+	glen = strfromf(got, sizeof(got), fmt, val);
+	ATF_CHECK_MSG(glen == rlen,
+	    "strfromf(\"%s\", %g): return %d, want %d",
+	    fmt, (double)val, glen, rlen);
+	ATF_CHECK_STREQ_MSG(ref, got, "strfromf(\"%s\", %g)", fmt, (double)val);
+}
+
+static void
+check_l(const char *fmt, long double val)
+{
+	char got[FBUF], ref[FBUF], lfmt[32];
+	int rlen, glen;
+
+	/*
+	 * snprintf requires the 'L' length modifier for long double.
+	 */
+	snprintf(lfmt, sizeof(lfmt), "%.*sL%c",
+	    (int)(strlen(fmt) - 1), fmt, fmt[strlen(fmt) - 1]);
+	rlen = snprintf(ref, sizeof(ref), lfmt, val);
+	glen = strfroml(got, sizeof(got), fmt, val);
+	ATF_CHECK_MSG(glen == rlen,
+	    "strfroml(\"%s\", %Lg): return %d, want %d", fmt, val, glen, rlen);
+	ATF_CHECK_STREQ_MSG(ref, got, "strfroml(\"%s\", %Lg)", fmt, val);
+}
*** 513 LINES SKIPPED ***


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a6f33cc.1cf60.548886f9>