assertf: Formatted asserts

assertf's behavior depends on the build:
- In release builds it executes recovery code.
- In debug builds it shows a crash screen. When start is pressed it
  resumes and executes the recovery code.
- In test builds it fails the test with an INVALID result.
This commit is contained in:
Martin Griffin 2025-11-10 13:21:54 +00:00
parent 0a5da344e3
commit cc8c8bd668
9 changed files with 510 additions and 15 deletions

View File

@ -191,6 +191,20 @@ else
}
```
The exception is `assertf` which should always use braces if it has a recovery path, even for one line of code.
```c
assertf(true); // correct
assertf(true) // correct
{
return NULL;
}
assertf(true) // incorrect
return NULL;
```
### Control Structures
When comparing whether or not a value equals `0`, don't be explicit unless the

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

View File

@ -0,0 +1,5 @@
JASC-PAL
0100
2
0 0 255
255 255 255

62
include/assertf.h Normal file
View File

@ -0,0 +1,62 @@
#ifndef ASSERTF_H
#define ASSERTF_H
/* Formatted assert.
*
* Asserts are a way to catch programmer errors at run-time. They should
* be used when both of the following are true:
* 1. It's impossible to catch the error at compile-time.
* 2. The error is caused only by the programmer.
* For example:
* - removeobject for a local ID that isn't in the object event
* templates is a programmer error because the object could never be
* spawned.
* - removeobject for a local ID that isn't spawned is not (necessarily)
* a programmer error because the object could have been despawned by
* the player.
* - Trying to choose a move from the Fight menu when it's disabled is
* not a programmer error because the player is able to try to choose
* the move.
* - The battle engine receiving a disabled move as the chosen move is
* a programmer error because it should have been rejected by the UI.
*
* When possible, prefer to catch errors at compile-time with things
* like STATIC_ASSERT rather than at run-time with assertf.
*
* assertf(cond);
* assertf(cond) { recovery... }
* assertf(cond, fmt, ...);
* assertf(cond, fmt, ...) { recovery... }
*
* If cond is FALSE:
* - In a release build: executes the recovery code (if any).
* - In a debug build: shows a resumable crash screen and executes the
* recovery code (if any).
* - In a test build: causes the test to be INVALID.
*
* Usually the recovery code makes the function do nothing, for example:
* - warp to a map that doesn't exist shouldn't warp anywhere.
* - addobject of a local ID that doesn't exist shouldn't add anything.
*
* But sometimes the function has to return something, in which case the
* recovery code does something that seems "reasonable", for example:
* - CreateMonWithGenderNatureLetter should ignore an illegal gender or
* letter. */
#define assertf(cond, ...) CAT(_ASSERTF, FIRST(__VA_OPT__(_FMT,) _COND))(cond __VA_OPT__(,) __VA_ARGS__)
#define _ASSERTF_COND(cond) for (bool32 _recover = !(cond); _recover && (_ASSERTF_HANDLE("%s:%d: %s", __FILE__, __LINE__, STR(cond)), TRUE); _recover = FALSE)
#define _ASSERTF_FMT(cond, fmt, ...) for (bool32 _recover = !(cond); _recover && (_ASSERTF_HANDLE("%s:%d: " fmt, __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__), TRUE); _recover = FALSE)
#if RELEASE
#define _ASSERTF_HANDLE(...) 0
#elif TESTING
#include "test_result.h"
#define _ASSERTF_HANDLE(fmt, ...) Test_ExitWithResult(TEST_RESULT_INVALID, 0, fmt, __VA_ARGS__)
#else
#define _ASSERTF_HANDLE(fmt, ...) AssertfCrashScreen(__builtin_return_address(0), fmt, __VA_ARGS__)
#endif
void AssertfCrashScreen(const void *return0, const char *fmt, ...);
#endif

View File

@ -5,6 +5,7 @@
#include <limits.h>
#include "config/general.h" // we need to define config before gba headers as print stuff needs the functions nulled before defines.
#include "gba/gba.h"
#include "assertf.h"
#include "gametypes.h"
#include "siirtc.h"
#include "fpmath.h"

View File

@ -1,25 +1,13 @@
#ifndef GUARD_TEST_H
#define GUARD_TEST_H
#include "test_result.h"
#include "test_runner.h"
#include "random.h"
#define MAX_PROCESSES 32 // See also tools/mgba-rom-test-hydra/main.c
#define RIGGED_RNG_COUNT 8
enum TestResult
{
TEST_RESULT_FAIL,
TEST_RESULT_PASS,
TEST_RESULT_ASSUMPTION_FAIL,
TEST_RESULT_INVALID,
TEST_RESULT_ERROR,
TEST_RESULT_TIMEOUT,
TEST_RESULT_CRASH,
TEST_RESULT_TODO,
TEST_RESULT_KNOWN_FAIL,
};
struct TestRunner
{
u32 (*estimateCost)(void *);
@ -106,7 +94,6 @@ void CB2_TestRunner(void);
void Test_ExpectedResult(enum TestResult);
void Test_ExpectLeaks(bool32);
void Test_ExpectCrash(bool32);
void Test_ExitWithResult(enum TestResult, u32 stopLine, const char *fmt, ...);
u32 SourceLine(u32 sourceLineOffset);
u32 SourceLineOffset(u32 sourceLine);
void SetupRiggedRng(u32 sourceLine, enum RandomTag randomTag, u32 value);

20
include/test_result.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef GUARD_TEST_RESULT_H
#define GUARD_TEST_RESULT_H
enum TestResult
{
TEST_RESULT_FAIL,
TEST_RESULT_PASS,
TEST_RESULT_ASSUMPTION_FAIL,
TEST_RESULT_INVALID,
TEST_RESULT_ERROR,
TEST_RESULT_TIMEOUT,
TEST_RESULT_CRASH,
TEST_RESULT_TODO,
TEST_RESULT_KNOWN_FAIL,
};
void Test_ExitWithResult_(enum TestResult, u32 stopLine, const void *return0, const char *fmt, ...);
#define Test_ExitWithResult(result, stopLine, ...) Test_ExitWithResult_(result, stopLine, __builtin_return_address(0), __VA_ARGS__)
#endif

401
src/assertf.c Normal file
View File

@ -0,0 +1,401 @@
#include <alloca.h>
#include <stdarg.h>
#include "global.h"
#include "bg.h"
#include "main.h"
#include "malloc.h"
#include "m4a.h"
#include "constants/characters.h"
#include "constants/rgb.h"
struct BitUnPackArgs
{
u16 compressedSize;
u8 compressedBits;
u8 decompressedBits;
u32 dataOffset:31;
u32 offsetZeros:1;
};
extern void BitUnPack(const void *src, void *dest, const struct BitUnPackArgs *);
static const u16 sPltt[2] = INCBIN_U16("graphics/crash_screen/palette.gbapal");
static const u32 sGlyphs1BPP[] = INCBIN_U32("graphics/crash_screen/font.1bpp");
enum
{
GLYPH_SPACE,
GLYPH_UNDERSCORE,
GLYPH_PERIOD,
GLYPH_COLON,
GLYPH_SLASH,
GLYPH_A,
GLYPH_0 = GLYPH_A + 26,
};
static const struct BitUnPackArgs sBitUnPack1BPP =
{
.compressedSize = sizeof(sGlyphs1BPP),
.compressedBits = 1,
.decompressedBits = 4,
.dataOffset = 0,
.offsetZeros = FALSE,
};
#define TILE0_OFFSET ((32 * 20 * sizeof(u16)) / TILE_SIZE_4BPP)
static bool32 Putc(u32 *x, u32 *y, char c)
{
if (c == '\n')
goto newline;
u32 glyph;
if ('a' <= c && c <= 'z')
glyph = GLYPH_A + c - 'a';
else if ('A' <= c && c <= 'Z')
glyph = GLYPH_A + c - 'A';
else if ('0' <= c && c <= '9')
glyph = GLYPH_0 + c - '0';
else if (c == ' ')
glyph = GLYPH_SPACE;
else if (c == '.')
glyph = GLYPH_PERIOD;
else if (c == ':')
glyph = GLYPH_COLON;
else if (c == '/')
glyph = GLYPH_SLASH;
else
glyph = GLYPH_UNDERSCORE;
((vu16 *)VRAM)[*x + *y * 32] = TILE0_OFFSET + glyph;
*x += 1;
if (*x == 30)
{
newline:
*x = 0;
*y += 1;
}
return *y < 18;
}
static bool32 Puti(u32 *x, u32 *y, s32 i)
{
if (i < 0)
{
if (!Putc(x, y, '-'))
return FALSE;
return Puti(x, y, -i);
}
else if (i == 0)
{
return Putc(x, y, '0');
}
u8 digits[9]; // floor(log10(INT_MAX))
u32 n = 0;
while (i)
{
digits[n++] = i % 10;
i /= 10;
}
while (n > 0)
{
n--;
if (!Putc(x, y, '0' + digits[n]))
return FALSE;
}
return TRUE;
}
static bool32 Putp(u32 *x, u32 *y, const void *p)
{
uintptr_t address = (uintptr_t)p;
u8 digits[8];
for (u32 n = 0; n < 8; n++)
{
digits[n] = address % 16;
address /= 16;
}
for (u32 n = 0; n < 8; n++)
{
u32 d = digits[7 - n];
// Most addresses are 7-digit, so elide the 8th if it's zero.
if (n == 0 && d == 0)
continue;
if (0 <= d && d <= 9)
{
if (!Putc(x, y, '0' + d))
return FALSE;
}
else
{
if (!Putc(x, y, 'A' + d - 10))
return FALSE;
}
}
return TRUE;
}
static bool32 Puts(u32 *x, u32 *y, const char *s)
{
while (*s != '\0')
{
if (!Putc(x, y, *s++))
return FALSE;
}
return TRUE;
}
static bool32 PutS(u32 *x, u32 *y, const u8 *s)
{
while (*s != EOS)
{
char c;
if (CHAR_a <= *s && *s <= CHAR_z)
c = 'A' + *s - CHAR_a;
else if (CHAR_A <= *s && *s <= CHAR_Z)
c = 'A' + *s - CHAR_A;
else if (CHAR_0 <= *s && *s <= CHAR_9)
c = '0' + *s - CHAR_0;
else if (*s == ' ')
c = ' ';
else if (*s == '.')
c = '.';
else if (*s == ':')
c = ':';
else
c = '_';
if (!Putc(x, y, c))
return FALSE;
}
return TRUE;
}
static bool32 Putx(u32 *x, u32 *y, unsigned u)
{
u8 digits[8];
u32 n = 0;
while (u)
{
digits[n++] = u % 16;
u /= 16;
}
while (n > 0)
{
n--;
if (0 <= digits[n] && digits[n] <= 9)
{
if (!Putc(x, y, '0' + digits[n]))
return FALSE;
}
else
{
if (!Putc(x, y, 'A' + digits[n] - 10))
return FALSE;
}
}
return TRUE;
}
// This printf renders directly into VRAM rather than into a buffer.
static void Vprintf(const void *return1, const void *return0, const char *fmt, va_list va)
{
u32 x, y;
x = 3;
y = 19;
static const char footer[] = "Press START to continue.";
for (u32 i = 0; i < sizeof(footer) - 1; i++)
Putc(&x, &y, footer[i]);
x = 0;
y = 0;
while (TRUE)
{
char c = *fmt++;
if (c == '\0')
{
break;
}
else if (c == '%')
{
char f = *fmt++;
switch (f)
{
case 'd':
if (!Puti(&x, &y, va_arg(va, int)))
return;
break;
case 'p':
if (!Putp(&x, &y, va_arg(va, const void *)))
return;
break;
case 's':
if (!Puts(&x, &y, va_arg(va, const char *)))
return;
break;
case 'S':
if (!PutS(&x, &y, va_arg(va, const u8 *)))
return;
break;
case 'x':
if (!Putx(&x, &y, va_arg(va, unsigned)))
return;
break;
}
}
else
{
if (!Putc(&x, &y, c))
return;
}
}
if (!Puts(&x, &y, "\n in: "))
return;
if (!Putp(&x, &y, return1))
return;
if (!Puts(&x, &y, "\n in: "))
return;
if (!Putp(&x, &y, return0))
return;
}
static void BusyWaitForVBlank(void)
{
// Interrupts are disabled so we have to busy loop to wait for
// v-blanks.
while (REG_VCOUNT < 160)
;
}
struct Backup
{
bool8 onHeap;
u8 ime;
u16 soundcnt_l;
u16 soundcnt_h;
u16 dispcnt;
u16 bg0cnt;
u16 bgPltt[2];
u8 vram[32 * 20 * sizeof(u16) + TILE_SIZE_4BPP + 4 * sizeof(sGlyphs1BPP)];
};
/* Blue Screen of Death style screen that displays the error message and
* hijacks the main loop until the start button is pressed. */
void AssertfCrashScreen(const void *return1, const char *fmt, ...)
{
// Backup and override hardware state.
struct Backup *backup = NULL;
// Allocate on heap if possible.
if (!backup)
{
backup = Alloc(sizeof(*backup));
if (backup)
backup->onHeap = TRUE;
}
// Allocate on stack if possible.
if (!backup)
{
extern char __iwram_end[];
size_t stack_free = (char *)__builtin_frame_address(0) - __iwram_end;
if (stack_free > sizeof(*backup) + 128)
{
backup = alloca(sizeof(*backup));
backup->onHeap = FALSE;
}
}
if (!backup)
{
// TODO: What to do?
return;
}
backup->ime = REG_IME;
REG_IME = 0;
m4aMPlayStop(&gMPlayInfo_BGM);
m4aMPlayStop(&gMPlayInfo_SE1);
m4aMPlayStop(&gMPlayInfo_SE2);
m4aMPlayStop(&gMPlayInfo_SE3);
backup->soundcnt_l = REG_SOUNDCNT_L;
REG_SOUNDCNT_L = 0;
backup->soundcnt_h = REG_SOUNDCNT_H;
REG_SOUNDCNT_H = REG_SOUNDCNT_H & ~(SOUND_A_RIGHT_OUTPUT | SOUND_A_LEFT_OUTPUT | SOUND_B_RIGHT_OUTPUT | SOUND_B_LEFT_OUTPUT);
backup->dispcnt = REG_DISPCNT;
REG_DISPCNT = 0;
backup->bg0cnt = REG_BG0CNT;
REG_BG0CNT = BGCNT_CHARBASE(0) | BGCNT_16COLOR | BGCNT_SCREENBASE(0) | BGCNT_TXT256x256;
REG_BG0HOFS = 0; // NOTE: REG_BG0HOFS is write-only.
REG_BG0VOFS = 0; // NOTE: REG_BG0VOFS is write-only.
memcpy(backup->bgPltt, (void *)BG_PLTT, sizeof(backup->bgPltt));
memcpy((void *)BG_PLTT, sPltt, sizeof(backup->bgPltt));
CpuFastCopy((void *)VRAM, &backup->vram, sizeof(backup->vram));
for (u32 i = 0; i < 32 * 20; i++)
((vu16 *)VRAM)[i] = TILE0_OFFSET;
BitUnPack(sGlyphs1BPP, (void *)(VRAM + TILE_OFFSET_4BPP(TILE0_OFFSET)), &sBitUnPack1BPP);
va_list va;
va_start(va, fmt);
Vprintf(return1, __builtin_return_address(0), fmt, va);
va_end(va);
BusyWaitForVBlank();
REG_DISPCNT = DISPCNT_MODE_0 | DISPCNT_BG0_ON;
u16 prevKeyinput = ~REG_KEYINPUT;
enum { WAIT_FOR_PRESS, WAIT_FOR_RELEASE } state = WAIT_FOR_PRESS;
while (TRUE)
{
BusyWaitForVBlank();
// Exit when start is pressed.
u16 keyinput = ~REG_KEYINPUT;
if (state == WAIT_FOR_PRESS)
{
if (!(prevKeyinput & START_BUTTON) && (keyinput & START_BUTTON))
state = WAIT_FOR_RELEASE;
}
else if (state == WAIT_FOR_RELEASE)
{
if ((prevKeyinput & START_BUTTON) && !(keyinput & START_BUTTON))
break;
}
prevKeyinput = keyinput;
}
// Restore backup.
REG_DISPCNT = 0;
CpuFastCopy(&backup->vram, (void *)VRAM, sizeof(backup->vram));
memcpy((void *)BG_PLTT, backup->bgPltt, sizeof(backup->bgPltt));
// Best-effort, restore BG0HOFS/BG0VOFS from one of GF's caches.
REG_BG0VOFS = GetBgY(0);
REG_BG0HOFS = GetBgX(0);
REG_BG0CNT = backup->bg0cnt;
REG_DISPCNT = backup->dispcnt;
REG_SOUNDCNT_H = backup->soundcnt_h;
REG_SOUNDCNT_L = backup->soundcnt_l;
m4aMPlayContinue(&gMPlayInfo_SE3);
m4aMPlayContinue(&gMPlayInfo_SE2);
m4aMPlayContinue(&gMPlayInfo_SE1);
m4aMPlayContinue(&gMPlayInfo_BGM);
REG_IME = backup->ime;
if (backup->onHeap)
Free(backup);
}

View File

@ -654,7 +654,7 @@ static void Intr_Timer2(void)
}
}
void Test_ExitWithResult(enum TestResult result, u32 stopLine, const char *fmt, ...)
void Test_ExitWithResult_(enum TestResult result, u32 stopLine, const void *return1, const char *fmt, ...)
{
gTestRunnerState.result = result;
gTestRunnerState.failedAssumptionsBlockLine = stopLine;
@ -665,6 +665,11 @@ void Test_ExitWithResult(enum TestResult result, u32 stopLine, const char *fmt,
if (!gTestRunnerState.test->runner->handleExitWithResult
|| !gTestRunnerState.test->runner->handleExitWithResult(gTestRunnerState.test->data, result))
{
if (result == TEST_RESULT_INVALID)
{
const void *return0 = __builtin_return_address(0);
Test_MgbaPrintf("in %p\nin %p", return1, return0);
}
va_list va;
va_start(va, fmt);
MgbaVPrintf_(fmt, va);