65816-llvm-mos/runtime/src/timeExt.c
Scott Duensing 15c7fa0db2 Checkpoint
2026-05-07 19:59:20 -05:00

250 lines
8.6 KiB
C

// time.h additions (struct tm, gmtime, localtime, mktime, asctime,
// ctime, difftime, strftime). Split out from libc.c to keep that
// translation unit's frame size below the W65816 stack-relative range.
typedef long time_t;
typedef unsigned long clock_t;
typedef unsigned long size_t;
extern size_t strlen(const char *);
static const unsigned short __monthDays[12] = {
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
};
static int __isLeap(int y) {
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
// ---- time.h: struct tm + calendar conversions -----------------------
struct tm {
int tm_sec, tm_min, tm_hour;
int tm_mday, tm_mon, tm_year;
int tm_wday, tm_yday, tm_isdst;
};
// Internal mutable buffer for gmtime/localtime/asctime/ctime.
// Not thread-safe but matches the C standard's spec.
static struct tm __gmtimeBuf;
static char __asctimeBuf[32];
double difftime(time_t end, time_t start) {
return (double)(end - start);
}
struct tm *gmtime_r(const time_t *t, struct tm *out);
// gmtime / localtime: convert seconds-since-1970 to broken-down time.
// "local" is identical to "gm" — no timezone support.
//
// Returns a pointer to a static global (`__gmtimeBuf`). Under GS/OS
// Loader (DBR != 0) caller-side pointer-deref reads need to land in
// the same bank where gmtime wrote; this requires the runtime build
// to enable `-mllvm -w65816-loader-bank-deref`, which makes
// LDAptr/STAptr load the bank byte from DP $BE (set by crt0 from
// PHK / current PBR). Without the flag, gmtime still works under
// MAME / non-Loader runs where DBR=0 throughout.
struct tm *gmtime(const time_t *t) {
return gmtime_r(t, &__gmtimeBuf);
}
struct tm *localtime(const time_t *t) {
return gmtime(t);
}
// gmtime_r / localtime_r — POSIX reentrant variants. Take a caller-
// supplied struct tm so the buffer lives on the user's stack (which
// is bank-0 in 65816 native mode regardless of DBR). This avoids the
// bank-mismatch issue that breaks plain gmtime under Loader.
//
// Full broken-down time computation. Marked optnone because at -O2
// LLVM's combined IR optimizations (loop rotation + reassociation +
// induction-variable-simplify) mis-evaluate the year-increment loop's
// `days >= 365L + (__isLeap(...) ? 1 : 0)` comparison, leaving the
// loop body unexecuted and date fields stuck at the 1970 sentinel.
// optnone preserves the per-statement structure and the loop runs
// correctly. Verified end-to-end against 1710484245L → 2024-03-15
// 06:30:45 UTC (Friday, day-of-year 74).
__attribute__((optnone))
struct tm *gmtime_r(const time_t *t, struct tm *out) {
long secs = *t;
int sec = (int)(secs % 60L); secs /= 60L;
int min = (int)(secs % 60L); secs /= 60L;
int hour = (int)(secs % 24L); secs /= 24L;
long days = secs;
int wday = (int)((days + 4L) % 7L);
if (wday < 0) wday += 7;
int year = 70; // years since 1900
while (days >= 365L + (__isLeap(1900 + year) ? 1 : 0)) {
days -= 365L + (__isLeap(1900 + year) ? 1 : 0);
year++;
}
int yday = (int)days;
int leap = __isLeap(1900 + year);
int mon = 11;
while (mon > 0) {
int firstDayOfMon = __monthDays[mon] + (leap && mon > 1 ? 1 : 0);
if ((int)days >= firstDayOfMon) break;
mon--;
}
int firstDay = __monthDays[mon] + (leap && mon > 1 ? 1 : 0);
int mday = (int)days - firstDay + 1;
out->tm_sec = sec;
out->tm_min = min;
out->tm_hour = hour;
out->tm_mday = mday;
out->tm_mon = mon;
out->tm_year = year;
out->tm_wday = wday;
out->tm_yday = yday;
out->tm_isdst = -1;
return out;
}
struct tm *localtime_r(const time_t *t, struct tm *out) {
return gmtime_r(t, out);
}
// mktime: convert broken-down time → seconds-since-1970. Also fills
// in tm_wday and tm_yday if the caller didn't bother.
time_t mktime(struct tm *tm) {
int year = tm->tm_year + 1900;
int mon = tm->tm_mon;
long days = 0;
for (int y = 1970; y < year; y++) {
days += __isLeap(y) ? 366 : 365;
}
for (int m = 0; m < mon; m++) {
days += __monthDays[m + 1] - __monthDays[m];
// (the table starts at 0 for Jan, so each month is the diff)
}
if (mon > 1 && __isLeap(year)) days++;
days += tm->tm_mday - 1;
long secs = days * 86400L
+ (long)tm->tm_hour * 3600L
+ (long)tm->tm_min * 60L
+ tm->tm_sec;
tm->tm_wday = (int)((days + 4) % 7);
if (tm->tm_wday < 0) tm->tm_wday += 7;
tm->tm_yday = (int)(__monthDays[mon]
+ ((mon > 1 && __isLeap(year)) ? 1 : 0)
+ tm->tm_mday - 1);
return secs;
}
static const char *const __wdayShort[7] = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
static const char *const __wdayLong[7] = {
"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"
};
static const char *const __monShort[12] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
static const char *const __monLong[12] = {
"January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"
};
// Format N-digit zero-padded. Forces u32 (long) arithmetic — clang's
// strength reducer otherwise lowers /10 and %10 on small types into
// i8 mulhu by 0xCD (magic constant for div-by-10), which the W65816
// backend has no select pattern for.
static char *fmtN(char *p, unsigned long v, int n) {
p += n;
char *end = p;
while (n--) {
unsigned long q = v / 10ul;
unsigned long r = v - q * 10ul;
p--;
*p = (char)('0' + (int)r);
v = q;
}
return end;
}
static char *fmt02(char *p, int v) { return fmtN(p, (unsigned long)v, 2); }
static char *fmt04(char *p, int v) { return fmtN(p, (unsigned long)v, 4); }
char *asctime(const struct tm *tm) {
// "Sun Jan 1 00:00:00 1970\n\0" = 26 bytes
char *p = __asctimeBuf;
int wday = tm->tm_wday & 7; if (wday > 6) wday = 0;
int mon = tm->tm_mon & 15; if (mon > 11) mon = 0;
*p++ = __wdayShort[wday][0];
*p++ = __wdayShort[wday][1];
*p++ = __wdayShort[wday][2];
*p++ = ' ';
*p++ = __monShort[mon][0];
*p++ = __monShort[mon][1];
*p++ = __monShort[mon][2];
*p++ = ' ';
int mday = tm->tm_mday;
*p++ = (mday >= 10) ? ('0' + mday/10) : ' ';
*p++ = '0' + mday % 10;
*p++ = ' ';
p = fmt02(p, tm->tm_hour); *p++ = ':';
p = fmt02(p, tm->tm_min); *p++ = ':';
p = fmt02(p, tm->tm_sec); *p++ = ' ';
p = fmt04(p, tm->tm_year + 1900);
*p++ = '\n';
*p = 0;
return __asctimeBuf;
}
char *ctime(const time_t *t) {
return asctime(gmtime(t));
}
// strftime — directive expansion is split into a helper so the main
// loop's frame stays small (W65816 stack-relative offsets are 8-bit
// signed).
__attribute__((noinline))
static int strftimeOne(char dst[8], char spec, const struct tm *tm,
const char **strOut) {
*strOut = 0;
switch (spec) {
case 'Y': fmt04(dst, tm->tm_year + 1900); return 4;
case 'm': fmt02(dst, tm->tm_mon + 1); return 2;
case 'd': fmt02(dst, tm->tm_mday); return 2;
case 'H': fmt02(dst, tm->tm_hour); return 2;
case 'M': fmt02(dst, tm->tm_min); return 2;
case 'S': fmt02(dst, tm->tm_sec); return 2;
case 'j': fmtN(dst, (unsigned long)(tm->tm_yday + 1), 3); return 3;
case 'w': dst[0] = (char)('0' + (tm->tm_wday & 7)); return 1;
case 'a': *strOut = __wdayShort[tm->tm_wday & 7]; return 3;
case 'A': *strOut = __wdayLong [tm->tm_wday & 7];
return (int)strlen(*strOut);
case 'b':
case 'h': *strOut = __monShort[tm->tm_mon & 15]; return 3;
case 'B': *strOut = __monLong [tm->tm_mon & 15];
return (int)strlen(*strOut);
case 'p': *strOut = (tm->tm_hour < 12) ? "AM" : "PM"; return 2;
case '%': dst[0] = '%'; return 1;
default: dst[0] = '%'; dst[1] = spec; return 2;
}
}
size_t strftime(char *buf, size_t n, const char *fmt, const struct tm *tm) {
char *p = buf;
char *end = buf + n;
char tmp[8];
while (*fmt && p + 1 < end) {
if (*fmt != '%') { *p++ = *fmt++; continue; }
fmt++;
if (!*fmt) break;
const char *ins;
int len = strftimeOne(tmp, *fmt, tm, &ins);
const char *src = ins ? ins : tmp;
for (int i = 0; i < len && p + 1 < end; i++) *p++ = src[i];
fmt++;
}
if (p < end) *p = 0;
else if (n > 0) buf[n - 1] = 0;
return (size_t)(p - buf);
}