349 lines
11 KiB
C
349 lines
11 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. This was originally a workaround for two
|
|
// W65816 backend bugs (since fixed):
|
|
// (1) The address of a stack-resident struct used to be computed
|
|
// wrong (&s came out as SP+5 = address of s.end instead of SP+3).
|
|
// (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.
|
|
// The struct-sink form now compiles correctly, but switching back to it
|
|
// would shift every TU's branch distances; left as-is for stability.
|
|
// Single-threaded use only, but that matches the rest of this runtime.
|
|
//
|
|
// Reverse-emit pattern (used by emitUDec / emitULong / emitHex): 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. Use the
|
|
// forward count + index-arithmetic form instead.
|
|
|
|
typedef unsigned long 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; see file header for the forward-index rationale.
|
|
int top = i;
|
|
for (int j = 0; j < top; j++) {
|
|
emit(buf[top - 1 - j]);
|
|
}
|
|
}
|
|
|
|
|
|
__attribute__((noinline))
|
|
static void emitDec(int n) {
|
|
// -n on INT_MIN is signed-overflow UB; negate as unsigned.
|
|
if (n < 0) {
|
|
emit('-');
|
|
emitUDec(0u - (unsigned int)n);
|
|
} else {
|
|
emitUDec((unsigned int)n);
|
|
}
|
|
}
|
|
|
|
|
|
__attribute__((noinline))
|
|
__attribute__((optnone))
|
|
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; see file header for the forward-index rationale.
|
|
int top = i;
|
|
for (int j = 0; j < top; j++) {
|
|
emit(buf[top - 1 - j]);
|
|
}
|
|
}
|
|
|
|
|
|
__attribute__((noinline,optnone))
|
|
static void emitSignedLong(long n) {
|
|
// See emitDec: avoid the signed-overflow UB on LONG_MIN.
|
|
if (n < 0) {
|
|
emit('-');
|
|
emitULong(0ul - (unsigned long)n);
|
|
} else {
|
|
emitULong((unsigned long)n);
|
|
}
|
|
}
|
|
|
|
|
|
__attribute__((noinline))
|
|
static void emitHex(unsigned int n, int width) {
|
|
static const char digits[] = "0123456789abcdef";
|
|
// unsigned int is 16-bit on this target -> at most 4 hex digits.
|
|
// Cap width to that; without it `snprintf("%08x", ...)` blew past
|
|
// the buf[] tail and corrupted the stack.
|
|
char buf[4];
|
|
if (width > 4) width = 4;
|
|
int i = 0;
|
|
if (n == 0) {
|
|
buf[i++] = '0';
|
|
}
|
|
while (n > 0 && i < 4) {
|
|
buf[i++] = digits[n & 0xF];
|
|
n >>= 4;
|
|
}
|
|
while (i < width) {
|
|
buf[i++] = '0';
|
|
}
|
|
// Reverse-emit; see file header for the forward-index rationale.
|
|
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;
|
|
}
|
|
// Avoid `if (v < 0)` (which calls __ltdf2) — the W65816 codegen
|
|
// for that comparison passes its double arg with a missing word,
|
|
// and the test silently returns false for negatives. Read the
|
|
// IEEE-754 sign bit and clear it inline instead.
|
|
unsigned long long bits;
|
|
__builtin_memcpy(&bits, &v, 8);
|
|
if (bits & ((unsigned long long)1 << 63)) {
|
|
emit('-');
|
|
bits &= ~((unsigned long long)1 << 63);
|
|
__builtin_memcpy(&v, &bits, 8);
|
|
}
|
|
// Avoid `v - (double)ipart` and `frac * 10.0`: those produced
|
|
// wrong results when chained in this function (likely a softfp
|
|
// libcall-ABI mismatch where the subdf3 return placement didn't
|
|
// match the muldf3 arg placement). Instead scale v by 10^prec in
|
|
// one chain, do integer division to split, and emit two fields.
|
|
unsigned long mul = 1;
|
|
for (int i = 0; i < prec; i++) {
|
|
v = v * 10.0;
|
|
mul *= 10;
|
|
}
|
|
// Round-half-up before truncation: 3.14 * 100 = 313.999... in
|
|
// soft-double, but `%.2f` of 3.14 should be "3.14" not "3.13".
|
|
// Adding 0.5 then truncating is equivalent to round-half-up for
|
|
// the non-negative `v` we have at this point.
|
|
v = v + 0.5;
|
|
// Cast via signed first; the runtime ships __fixdfsi but not
|
|
// __fixunsdfsi. v has been forced non-negative above so the
|
|
// signed cast loses no value range we care about.
|
|
unsigned long scaled = (unsigned long)(long)v;
|
|
unsigned long intPart = scaled / mul;
|
|
unsigned long frcPart = scaled - intPart * mul;
|
|
emitULong(intPart);
|
|
if (prec == 0) {
|
|
return;
|
|
}
|
|
emit('.');
|
|
// Emit `frcPart` as `prec` digits with leading zeros. Build into
|
|
// a small buffer in reverse, then emit forward (countdown loops
|
|
// are still suspect — see the reverse-emit comment above).
|
|
char buf[10];
|
|
for (int i = prec - 1; i >= 0; i--) {
|
|
buf[i] = (char)('0' + (frcPart % 10));
|
|
frcPart /= 10;
|
|
}
|
|
for (int i = 0; i < prec; i++) {
|
|
emit(buf[i]);
|
|
}
|
|
}
|
|
|
|
|
|
// fmt is arg0 (A register); see banner comment for why the order matters.
|
|
// Previously optnone (slot-alias bug under p:16:16; see
|
|
// feedback_snprintf_va_arg_slot_alias.md). Re-enabled greedy under
|
|
// ptr32 — testing whether the bug recurs.
|
|
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) {
|
|
// Truncated, but n > 0: overwrite the last byte with NUL so
|
|
// the result is a valid C string. snprintf with n=0 sets
|
|
// gEnd = NULL up front so this branch correctly skips —
|
|
// previously it wrote `gEnd[-1]` to `buf[-1]`, clobbering
|
|
// memory before the buffer.
|
|
gEnd[-1] = '\0';
|
|
}
|
|
return (int)gTotal;
|
|
}
|
|
|
|
|
|
|
|
__attribute__((optnone))
|
|
int snprintf(char *buf, size_t n, const char *fmt, ...) {
|
|
gCur = buf;
|
|
// n == 0 must NOT touch the buffer (C99 7.19.6.5). Setting
|
|
// gEnd = NULL here makes both `gCur < gEnd` and `gEnd > 0`
|
|
// false, so no NUL terminator gets written.
|
|
gEnd = n ? buf + n : (char *)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 = n ? buf + n : (char *)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);
|
|
}
|