65816-llvm-mos/runtime/src/snprintf.c
Scott Duensing d6c9fc8252 Checkpoint.
2026-04-30 20:48:41 -05:00

331 lines
9 KiB
C

// Buffer-formatting siblings of printf — kept in their own translation
// unit so the shared writeXxx helpers don't have to take a function-
// pointer sink (indirect call cost on this target) and so adding the
// formatter to libc.c can't shift vprintf's branch distances out of
// range (per the strtol.c precedent).
//
// Functions:
// int vsnprintf(char *buf, size_t n, const char *fmt, va_list ap);
// int snprintf (char *buf, size_t n, const char *fmt, ...);
// int vsprintf (char *buf, const char *fmt, va_list ap);
// int sprintf (char *buf, const char *fmt, ...);
//
// Format support matches vprintf: %d %i %u %x %X %c %s %p %f and the
// `l` length modifier (%ld %lu). Width is honoured for %x. %f
// precision is capped at 9 fractional digits.
//
// Return value: number of characters that would have been written had
// the buffer been unbounded (C99 vsnprintf semantics), not just the
// number actually written. This lets callers detect truncation.
//
// **Sink state lives in file-static globals** instead of an explicit
// struct passed by pointer. Two W65816 backend bugs forced this:
// (1) The address of a stack-resident struct is computed wrong
// (&s came out as SP+5 = address of s.end instead of SP+3).
// emit() then read garbage cur/end values, the cur >= end branch
// skipped every write, and snprintf returned the right length
// with an empty buffer.
// (2) Functions taking fmt as arg1 (stack) didn't initialize the
// fmt local before the loop body — first char came from the
// arg slot but the loop's fmt++ ran on uninitialized memory.
// Fixed by making fmt always arg0 (A reg).
// Single-threaded use only, but that matches the rest of this runtime.
typedef unsigned int size_t;
typedef __builtin_va_list va_list;
#define va_start(ap, last) __builtin_va_start(ap, last)
#define va_arg(ap, ty) __builtin_va_arg(ap, ty)
#define va_end(ap) __builtin_va_end(ap)
static char *gCur;
static char *gEnd;
static size_t gTotal;
__attribute__((noinline))
static void emit(char c) {
if (gCur < gEnd) {
*gCur++ = c;
}
gTotal++;
}
__attribute__((noinline))
static void emitStr(const char *p) {
if (!p) {
p = "(null)";
}
while (*p) {
emit(*p++);
}
}
__attribute__((noinline))
static void emitUDec(unsigned int n) {
char buf[6];
int i = 0;
if (n == 0) {
emit('0');
return;
}
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
// Reverse-emit using forward index arithmetic. The natural
// countdown forms (`while (i > 0) emit(buf[--i])`,
// `while (i > 0) { i--; emit(buf[i]); }`,
// `for (j = i - 1; j >= 0; j--) emit(buf[j])`) all lower to a
// do-while whose `dec a; bpl` exit condition runs the loop one
// extra time on this backend, leaking a buf[-1] read. The forward
// count + index-arithmetic form below avoids the bad lowering.
int top = i;
for (int j = 0; j < top; j++) {
emit(buf[top - 1 - j]);
}
}
__attribute__((noinline))
static void emitDec(int n) {
if (n < 0) {
emit('-');
emitUDec((unsigned int)(-n));
} else {
emitUDec((unsigned int)n);
}
}
__attribute__((noinline))
static void emitULong(unsigned long n) {
char buf[11];
int i = 0;
if (n == 0) {
emit('0');
return;
}
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
// Reverse-emit using forward index arithmetic. The natural
// countdown forms (`while (i > 0) emit(buf[--i])`,
// `while (i > 0) { i--; emit(buf[i]); }`,
// `for (j = i - 1; j >= 0; j--) emit(buf[j])`) all lower to a
// do-while whose `dec a; bpl` exit condition runs the loop one
// extra time on this backend, leaking a buf[-1] read. The forward
// count + index-arithmetic form below avoids the bad lowering.
int top = i;
for (int j = 0; j < top; j++) {
emit(buf[top - 1 - j]);
}
}
__attribute__((noinline))
static void emitSignedLong(long n) {
if (n < 0) {
emit('-');
emitULong((unsigned long)(-n));
} else {
emitULong((unsigned long)n);
}
}
__attribute__((noinline))
static void emitHex(unsigned int n, int width) {
static const char digits[] = "0123456789abcdef";
char buf[5];
int i = 0;
if (n == 0) {
buf[i++] = '0';
}
while (n > 0) {
buf[i++] = digits[n & 0xF];
n >>= 4;
}
while (i < width) {
buf[i++] = '0';
}
// Reverse-emit using forward index arithmetic. The natural
// countdown forms (`while (i > 0) emit(buf[--i])`,
// `while (i > 0) { i--; emit(buf[i]); }`,
// `for (j = i - 1; j >= 0; j--) emit(buf[j])`) all lower to a
// do-while whose `dec a; bpl` exit condition runs the loop one
// extra time on this backend, leaking a buf[-1] read. The forward
// count + index-arithmetic form below avoids the bad lowering.
int top = i;
for (int j = 0; j < top; j++) {
emit(buf[top - 1 - j]);
}
}
__attribute__((noinline))
static void emitDouble(double v, int prec) {
if (prec < 0) {
prec = 6;
}
if (prec > 9) {
prec = 9;
}
if (v < 0) {
emit('-');
v = -v;
}
long ipart = (long)v;
emitULong((unsigned long)ipart);
if (prec == 0) {
return;
}
emit('.');
double frac = v - (double)ipart;
long mul = 1;
for (int i = 0; i < prec; i++) {
mul *= 10;
}
long fdigits = (long)(frac * (double)mul);
if (fdigits < 0) {
fdigits = -fdigits;
}
char buf[10];
int n = 0;
long scale = mul / 10;
while (n < prec) {
if (scale == 0) {
scale = 1;
}
long d = fdigits / scale;
buf[n++] = '0' + (char)(d % 10);
scale /= 10;
if (scale == 0) {
break;
}
}
while (n < prec) {
buf[n++] = '0';
}
for (int i = 0; i < n; i++) {
emit(buf[i]);
}
}
// fmt is arg0 (A register); see banner comment for why the order matters.
static int format(const char *fmt, va_list ap) {
while (*fmt) {
char c = *fmt++;
if (c != '%') {
emit(c);
continue;
}
int width = 0;
while (*fmt >= '0' && *fmt <= '9') {
width = width * 10 + (*fmt - '0');
fmt++;
}
int prec = -1;
if (*fmt == '.') {
fmt++;
prec = 0;
while (*fmt >= '0' && *fmt <= '9') {
prec = prec * 10 + (*fmt - '0');
fmt++;
}
}
int isLong = 0;
if (*fmt == 'l') {
isLong = 1;
fmt++;
}
char spec = *fmt++;
if (spec == 'd' || spec == 'i') {
if (isLong) {
emitSignedLong(va_arg(ap, long));
} else {
emitDec(va_arg(ap, int));
}
} else if (spec == 'u') {
if (isLong) {
emitULong(va_arg(ap, unsigned long));
} else {
emitUDec(va_arg(ap, unsigned int));
}
} else if (spec == 'x' || spec == 'X') {
emitHex(va_arg(ap, unsigned int), width);
} else if (spec == 'c') {
emit((char)va_arg(ap, int));
} else if (spec == 's') {
emitStr(va_arg(ap, const char *));
} else if (spec == 'f' || spec == 'F' ||
spec == 'g' || spec == 'G' ||
spec == 'e' || spec == 'E') {
emitDouble(va_arg(ap, double), prec);
} else if (spec == 'p') {
emit('0');
emit('x');
emitHex(va_arg(ap, unsigned int), 4);
} else if (spec == '%') {
emit('%');
} else {
emit('%');
emit(spec);
}
}
if (gCur < gEnd) {
*gCur = '\0';
} else if (gEnd > (char *)0) {
gEnd[-1] = '\0';
}
return (int)gTotal;
}
int snprintf(char *buf, size_t n, const char *fmt, ...) {
gCur = buf;
gEnd = buf + (n ? n : 0);
gTotal = 0;
va_list ap;
va_start(ap, fmt);
int r = format(fmt, ap);
va_end(ap);
return r;
}
int sprintf(char *buf, const char *fmt, ...) {
gCur = buf;
// sprintf is unbounded. Setting gEnd = buf + 0xFFFE looks innocuous
// but clang lowers the +0xFFFE to a `dec a; dec a` peephole (since
// 0xFFFE is -2 in 16-bit), giving gEnd = buf - 2 — and then the
// emit() bounds test `cur < end` is always false, so nothing gets
// written. Use the absolute top-of-bank sentinel instead.
gEnd = (char *)0xFFFF;
gTotal = 0;
va_list ap;
va_start(ap, fmt);
int r = format(fmt, ap);
va_end(ap);
return r;
}
int vsnprintf(char *buf, size_t n, const char *fmt, va_list ap) {
gCur = buf;
gEnd = buf + (n ? n : 0);
gTotal = 0;
return format(fmt, ap);
}
int vsprintf(char *buf, const char *fmt, va_list ap) {
gCur = buf;
gEnd = (char *)0xFFFF;
gTotal = 0;
return format(fmt, ap);
}