shithub: rgbds

Download patch

ref: 66512ed8d2676f4937b2c64c9fea125aeb76d1d4
parent: 03967bd6238e13acdd91018fc1b27472d28eb39b
parent: fb58166e5d391643906fd0aabd58d367507426fa
author: Eldred Habert <[email protected]>
date: Sat Mar 21 19:06:44 EDT 2020

Merge pull request #488 from rednex/assert

Add assertions

--- a/include/asm/output.h
+++ b/include/asm/output.h
@@ -20,6 +20,8 @@
 
 void out_SetFileName(char *s);
 void out_CreatePatch(uint32_t type, struct Expression const *expr);
+bool out_CreateAssert(enum AssertionType type, struct Expression const *expr,
+		      char const *message);
 void out_WriteObject(void);
 
 #endif /* RGBDS_ASM_OUTPUT_H */
--- a/include/asm/warning.h
+++ b/include/asm/warning.h
@@ -22,6 +22,7 @@
 	WARNING_OBSOLETE,
 	WARNING_SHIFT,
 	WARNING_USER,
+	WARNING_ASSERT,
 	WARNING_SHIFT_AMOUNT,
 	WARNING_TRUNCATION,
 
--- a/include/link/patch.h
+++ b/include/link/patch.h
@@ -10,6 +10,28 @@
 #ifndef RGBDS_LINK_PATCH_H
 #define RGBDS_LINK_PATCH_H
 
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "link/section.h"
+
+#include "linkdefs.h"
+
+struct Assertion {
+	struct Patch patch;
+	// enum AssertionType type; The `patch`'s field is instead re-used
+	struct Section *section;
+	char *message;
+
+	struct Assertion *next;
+};
+
+/**
+ * Checks all assertions
+ * @return true if assertion failed
+ */
+void patch_CheckAssertions(struct Assertion *assertion);
+
 /**
  * Applies all SECTIONs' patches to them
  */
--- a/include/linkdefs.h
+++ b/include/linkdefs.h
@@ -14,7 +14,13 @@
 
 #define RGBDS_OBJECT_VERSION_STRING "RGB%1hhu"
 #define RGBDS_OBJECT_VERSION_NUMBER (uint8_t)9
-#define RGBDS_OBJECT_REV 1
+#define RGBDS_OBJECT_REV 2
+
+enum AssertionType {
+	ASSERT_WARN,
+	ASSERT_ERROR,
+	ASSERT_FATAL
+};
 
 enum RPNCommand {
 	RPN_ADD		= 0x00,
--- a/src/asm/asmy.y
+++ b/src/asm/asmy.y
@@ -23,6 +23,7 @@
 #include "asm/macro.h"
 #include "asm/main.h"
 #include "asm/mymath.h"
+#include "asm/output.h"
 #include "asm/rpn.h"
 #include "asm/section.h"
 #include "asm/symbol.h"
@@ -497,6 +498,7 @@
 	int32_t nConstValue;
 	struct SectionSpec sectSpec;
 	struct MacroArgs *macroArg;
+	enum AssertionType assertType;
 }
 
 %type	<sVal>		relocexpr
@@ -587,6 +589,8 @@
 %token	T_POP_LOAD T_POP_ENDL
 %token	T_POP_FAIL
 %token	T_POP_WARN
+%token	T_POP_FATAL
+%token	T_POP_ASSERT T_POP_STATIC_ASSERT
 %token	T_POP_PURGE
 %token	T_POP_POPS
 %token	T_POP_PUSHS
@@ -638,6 +642,7 @@
 %type	<nConstValue>	op_a_r
 %type	<nConstValue>	op_hl_ss
 %type	<sVal>		op_mem_ind
+%type	<assertType>	assert_type
 %start asmfile
 
 %%
@@ -759,6 +764,7 @@
 		| shift
 		| fail
 		| warn
+		| assert
 		| purge
 		| pops
 		| pushs
@@ -799,8 +805,97 @@
 warn		: T_POP_WARN string	{ warning(WARNING_USER, "%s", $2); }
 ;
 
+assert_type	: /* empty */		{ $$ = ASSERT_ERROR; }
+		| T_POP_WARN ','	{ $$ = ASSERT_WARN; }
+		| T_POP_FAIL ','	{ $$ = ASSERT_ERROR; }
+		| T_POP_FATAL ','	{ $$ = ASSERT_FATAL; }
+;
+
+assert		: T_POP_ASSERT assert_type relocexpr
+		{
+			if (rpn_isKnown(&$3) && $3.nVal == 0) {
+				switch ($2) {
+					case ASSERT_FATAL:
+						fatalerror("Assertion failed");
+					case ASSERT_ERROR:
+						yyerror("Assertion failed");
+						break;
+					case ASSERT_WARN:
+						warning(WARNING_ASSERT,
+							"Assertion failed");
+						break;
+				}
+			} else {
+				if (!out_CreateAssert($2, &$3, ""))
+					yyerror("Assertion creation failed: %s",
+						strerror(errno));
+			}
+			rpn_Free(&$3);
+		}
+		| T_POP_ASSERT assert_type relocexpr ',' string
+		{
+			if (rpn_isKnown(&$3) && $3.nVal == 0) {
+				switch ($2) {
+					case ASSERT_FATAL:
+						fatalerror("Assertion failed: %s",
+							   $5);
+					case ASSERT_ERROR:
+						yyerror("Assertion failed: %s",
+							$5);
+						break;
+					case ASSERT_WARN:
+						warning(WARNING_ASSERT,
+							"Assertion failed: %s",
+							$5);
+						break;
+				}
+			} else {
+				if (!out_CreateAssert($2, &$3, $5))
+					yyerror("Assertion creation failed: %s",
+						strerror(errno));
+			}
+			rpn_Free(&$3);
+		}
+		| T_POP_STATIC_ASSERT assert_type const
+		{
+			if ($3 == 0) {
+				switch ($2) {
+					case ASSERT_FATAL:
+						fatalerror("Assertion failed");
+					case ASSERT_ERROR:
+						yyerror("Assertion failed");
+						break;
+					case ASSERT_WARN:
+						warning(WARNING_ASSERT,
+							"Assertion failed");
+						break;
+				}
+			}
+		}
+		| T_POP_STATIC_ASSERT assert_type const ',' string
+		{
+			if ($3 == 0) {
+				switch ($2) {
+					case ASSERT_FATAL:
+						fatalerror("Assertion failed: %s",
+							   $5);
+					case ASSERT_ERROR:
+						yyerror("Assertion failed: %s",
+							$5);
+						break;
+					case ASSERT_WARN:
+						warning(WARNING_ASSERT,
+							"Assertion failed: %s",
+							$5);
+						break;
+				}
+			}
+		}
+;
+
 shift		: T_POP_SHIFT		{ macro_ShiftCurrentArgs(); }
-		| T_POP_SHIFT uconst {
+		| T_POP_SHIFT uconst
+		{
 			int32_t i = $2;
 			while (i--)
 				macro_ShiftCurrentArgs();
--- a/src/asm/globlex.c
+++ b/src/asm/globlex.c
@@ -496,6 +496,9 @@
 
 	{"fail", T_POP_FAIL},
 	{"warn", T_POP_WARN},
+	{"fatal", T_POP_FATAL},
+	{"assert", T_POP_ASSERT},
+	{"static_assert", T_POP_STATIC_ASSERT},
 
 	{"macro", T_POP_MACRO},
 	/* Not needed but we have it here just to protect the name */
--- a/src/asm/output.c
+++ b/src/asm/output.c
@@ -47,10 +47,18 @@
 	struct PatchSymbol *pBucketNext; /* next symbol in hash table bucket */
 };
 
+struct Assertion {
+	struct Patch *patch;
+	struct Section *section;
+	char *message;
+	struct Assertion *next;
+};
+
 struct PatchSymbol *tHashedPatchSymbols[HASHSIZE];
 struct Section *pSectionList, *pCurrentSection;
 struct PatchSymbol *pPatchSymbols;
 struct PatchSymbol **ppPatchSymbolsTail = &pPatchSymbols;
+struct Assertion *assertions = NULL;
 char *tzObjectname;
 
 /*
@@ -104,6 +112,21 @@
 	return r;
 }
 
+/**
+ * Count the number of assertions used in this object
+ */
+static uint32_t countasserts(void)
+{
+	struct Assertion *assert = assertions;
+	uint32_t count = 0;
+
+	while (assert) {
+		count++;
+		assert = assert->next;
+	}
+	return count;
+}
+
 /*
  * Write a long to a file (little-endian)
  */
@@ -287,62 +310,29 @@
 	}
 }
 
-/*
- * Allocate a new patchstructure and link it into the list
- */
-struct Patch *allocpatch(void)
+static void writerpn(uint8_t *rpnexpr, uint32_t *rpnptr, uint8_t *rpn,
+		     uint32_t rpnlen)
 {
-	struct Patch *pPatch;
-
-	pPatch = malloc(sizeof(struct Patch));
-
-	if (pPatch == NULL)
-		fatalerror("No memory for patch");
-
-	pPatch->pNext = pCurrentSection->pPatches;
-	pPatch->nRPNSize = 0;
-	pPatch->pRPN = NULL;
-	pCurrentSection->pPatches = pPatch;
-
-	return pPatch;
-}
-
-/*
- * Create a new patch (includes the rpn expr)
- */
-void out_CreatePatch(uint32_t type, struct Expression const *expr)
-{
-	struct Patch *pPatch;
-	uint8_t *rpnexpr;
 	char tzSym[512];
-	uint32_t rpnptr = 0, symptr;
 
-	rpnexpr = malloc(expr->nRPNPatchSize);
+	for (size_t offset = 0; offset < rpnlen; ) {
+#define popbyte() rpn[offset++]
+#define writebyte(byte)	rpnexpr[(*rpnptr)++] = byte
+		uint8_t rpndata = popbyte();
 
-	if (rpnexpr == NULL)
-		fatalerror("No memory for patch RPN expression");
-
-	pPatch = allocpatch();
-	pPatch->nType = type;
-	fstk_DumpToStr(pPatch->tzFilename, sizeof(pPatch->tzFilename));
-	pPatch->nOffset = pCurrentSection->nPC;
-
-	for (size_t offset = 0; offset < expr->nRPNLength; ) {
-#define popbyte(expr) (expr)->tRPN[offset++]
-		uint8_t rpndata = popbyte(expr);
-
 		switch (rpndata) {
 		case RPN_CONST:
-			rpnexpr[rpnptr++] = RPN_CONST;
-			rpnexpr[rpnptr++] = popbyte(expr);
-			rpnexpr[rpnptr++] = popbyte(expr);
-			rpnexpr[rpnptr++] = popbyte(expr);
-			rpnexpr[rpnptr++] = popbyte(expr);
+			writebyte(RPN_CONST);
+			writebyte(popbyte());
+			writebyte(popbyte());
+			writebyte(popbyte());
+			writebyte(popbyte());
 			break;
 		case RPN_SYM:
 		{
-			symptr = 0;
-			while ((tzSym[symptr++] = popbyte(expr)) != 0)
+			uint32_t symptr = 0;
+
+			while ((tzSym[symptr++] = popbyte()) != 0)
 				;
 
 			struct sSymbol const *sym = sym_FindSymbol(tzSym);
@@ -353,18 +343,18 @@
 				uint32_t value;
 
 				value = sym_GetConstantValue(tzSym);
-				rpnexpr[rpnptr++] = RPN_CONST;
-				rpnexpr[rpnptr++] = value & 0xFF;
-				rpnexpr[rpnptr++] = value >> 8;
-				rpnexpr[rpnptr++] = value >> 16;
-				rpnexpr[rpnptr++] = value >> 24;
+				writebyte(RPN_CONST);
+				writebyte(value & 0xFF);
+				writebyte(value >> 8);
+				writebyte(value >> 16);
+				writebyte(value >> 24);
 			} else {
 				symptr = addsymbol(sym);
-				rpnexpr[rpnptr++] = RPN_SYM;
-				rpnexpr[rpnptr++] = symptr & 0xFF;
-				rpnexpr[rpnptr++] = symptr >> 8;
-				rpnexpr[rpnptr++] = symptr >> 16;
-				rpnexpr[rpnptr++] = symptr >> 24;
+				writebyte(RPN_SYM);
+				writebyte(symptr & 0xFF);
+				writebyte(symptr >> 8);
+				writebyte(symptr >> 16);
+				writebyte(symptr >> 24);
 			}
 			break;
 		}
@@ -371,9 +361,9 @@
 		case RPN_BANK_SYM:
 		{
 			struct sSymbol *sym;
+			uint32_t symptr = 0;
 
-			symptr = 0;
-			while ((tzSym[symptr++] = popbyte(expr)) != 0)
+			while ((tzSym[symptr++] = popbyte()) != 0)
 				;
 
 			sym = sym_FindSymbol(tzSym);
@@ -381,11 +371,11 @@
 				break;
 
 			symptr = addsymbol(sym);
-			rpnexpr[rpnptr++] = RPN_BANK_SYM;
-			rpnexpr[rpnptr++] = symptr & 0xFF;
-			rpnexpr[rpnptr++] = symptr >> 8;
-			rpnexpr[rpnptr++] = symptr >> 16;
-			rpnexpr[rpnptr++] = symptr >> 24;
+			writebyte(RPN_BANK_SYM);
+			writebyte(symptr & 0xFF);
+			writebyte(symptr >> 8);
+			writebyte(symptr >> 16);
+			writebyte(symptr >> 24);
 			break;
 		}
 		case RPN_BANK_SECT:
@@ -392,28 +382,95 @@
 		{
 			uint16_t b;
 
-			rpnexpr[rpnptr++] = RPN_BANK_SECT;
+			writebyte(RPN_BANK_SECT);
 
 			do {
-				b = popbyte(expr);
-				rpnexpr[rpnptr++] = b & 0xFF;
+				b = popbyte();
+				writebyte(b & 0xFF);
 			} while (b != 0);
 			break;
 		}
 		default:
-			rpnexpr[rpnptr++] = rpndata;
+			writebyte(rpndata);
 			break;
 		}
 #undef popbyte
+#undef writebyte
 	}
+}
 
-	assert(rpnptr == expr->nRPNPatchSize);
+/*
+ * Allocate a new patch structure and link it into the list
+ */
+static struct Patch *allocpatch(uint32_t type, struct Expression const *expr)
+{
+	struct Patch *pPatch;
 
-	pPatch->pRPN = rpnexpr;
-	pPatch->nRPNSize = rpnptr;
+	pPatch = malloc(sizeof(struct Patch));
+
+	if (!pPatch)
+		fatalerror("No memory for patch: %s", strerror(errno));
+	pPatch->pRPN = malloc(sizeof(*pPatch->pRPN) * expr->nRPNPatchSize);
+
+	if (!pPatch->pRPN)
+		fatalerror("No memory for patch's RPN expression: %s",
+			   strerror(errno));
+
+	pPatch->nRPNSize = 0;
+	pPatch->nType = type;
+	pPatch->nOffset = pCurrentSection->nPC;
+	fstk_DumpToStr(pPatch->tzFilename, sizeof(pPatch->tzFilename));
+
+	writerpn(pPatch->pRPN, &pPatch->nRPNSize, expr->tRPN, expr->nRPNLength);
+	assert(pPatch->nRPNSize == expr->nRPNPatchSize);
+
+	return pPatch;
 }
 
 /*
+ * Create a new patch (includes the rpn expr)
+ */
+void out_CreatePatch(uint32_t type, struct Expression const *expr)
+{
+	struct Patch *pPatch = allocpatch(type, expr);
+
+	pPatch->pNext = pCurrentSection->pPatches;
+	pCurrentSection->pPatches = pPatch;
+}
+
+/**
+ * Creates an assert that will be written to the object file
+ */
+bool out_CreateAssert(enum AssertionType type, struct Expression const *expr,
+		      char const *message)
+{
+	struct Assertion *assertion = malloc(sizeof(*assertion));
+
+	if (!assertion)
+		return false;
+
+	assertion->patch = allocpatch(type, expr);
+	assertion->section = pCurrentSection;
+	assertion->message = strdup(message);
+	if (!assertion->message) {
+		free(assertion);
+		return false;
+	}
+
+	assertion->next = assertions;
+	assertions = assertion;
+
+	return true;
+}
+
+static void writeassert(struct Assertion *assert, FILE *f)
+{
+	writepatch(assert->patch, f);
+	fputlong(getsectid(assert->section), f);
+	fputstring(assert->message, f);
+}
+
+/*
  * Write an objectfile
  */
 void out_WriteObject(void)
@@ -421,6 +478,7 @@
 	FILE *f;
 	struct PatchSymbol *pSym;
 	struct Section *pSect;
+	struct Assertion *assert = assertions;
 
 	addexports();
 
@@ -444,6 +502,12 @@
 	while (pSect) {
 		writesection(pSect, f);
 		pSect = pSect->pNext;
+	}
+
+	fputlong(countasserts(), f);
+	while (assert) {
+		writeassert(assert, f);
+		assert = assert->next;
 	}
 
 	fclose(f);
--- a/src/asm/rgbasm.1
+++ b/src/asm/rgbasm.1
@@ -141,7 +141,8 @@
 The following warnings are actual warning flags; with each description, the corresponding warning flag is included.
 Note that each of these flag also has a negation (for example,
 .Fl Wempty-entry
-enables the warning that Fl Wno-empty-entry
+enables the warning that
+.Fl Wno-empty-entry
 disables).
 Only the non-default flag is listed here.
 Ignoring the
@@ -148,6 +149,13 @@
 .Dq no-
 prefix, entries are listed alphabetically.
 .Bl -tag -width Ds
+.It Fl Wno-assert
+Warns when
+.Ic WARN Ns No -type
+assertions fail. (See
+.Xr rgbasm 5 "Aborting the assembly process"
+for
+.Ic ASSERT ) .
 .It Fl Wbuiltin-args
 Warn about incorrect arguments to built-in functions, such as
 .Fn STRSUB
@@ -182,9 +190,12 @@
 Warn when shifting triggers C undefined behavior, potentially causing unpredictable behavior.
 Shfting behavior will be changed and this warning removed before next release.
 .It Fl Wno-user
-Warns when the built-in function
-.Fn WARN
-is executed.
+Warns when the
+.Ic WARN
+built-in is executed. (See
+.Xr rgbasm 5 "Aborting the assembly process"
+for
+.Ic WARN ) .
 .El
 .Sh EXAMPLES
 You can assemble a source file in two ways.
--- a/src/asm/rgbasm.5
+++ b/src/asm/rgbasm.5
@@ -980,6 +980,62 @@
 stops assembling immediately while
 .Ic WARN
 shows the message but continues afterwards.
+.Pp
+If you need to ensure some assumption is correct when compiling, you can use
+.Ic ASSERT
+and
+.Ic STATIC_ASSERT .
+Syntax examples are given below:
+.Pp
+.Bd -literal -offset indent
+Function:
+      xor a
+ASSERT LOW(Variable) == 0
+      ld h, HIGH(Variable)
+      ld l, a
+      ld a, [hli]
+      ; You can also indent this!
+      ASSERT BANK(OtherFunction) == BANK(Function)
+      call OtherFunction
+; Lowercase also works
+assert Variable + 1 == OtherVariable
+      ld c, [hl]
+      ret
+\&.end
+      ; If you specify one, a message will be printed
+      STATIC_ASSERT .end - Function < 256, "Function is too large!"
+.Ed
+.Pp
+First, the difference between
+.Ic ASSERT
+and
+.Ic STATIC_ASSERT
+is that the former is evaluated by RGBASM if it can, otherwise by RGBLINK; but the latter is only ever evaluated by RGBASM.
+If RGBASM cannot compute the value of the argument to
+.Ic STATIC_ASSERT ,
+it will produce an error.
+.Pp
+Second, as shown above, a string can be optionally added at the end, to give insight into what the assertion is checking.
+.Pp
+Finally, you can add one of
+.Ic WARN , FAIL
+or
+.Ic FATAL
+as the first optional argument to either
+.Ic ASSERT
+or
+.Ic STATIC_ASSERT .
+If the assertion fails,
+.Ic WARN
+will cause a simple warning (controlled by
+.Xr rgbasm 1 DIAGNOSTICS
+flag
+.Fl Wassert )
+to be emitted;
+.Ic FAIL
+(the default) will cause a non-fatal error; and
+.Ic FATAL
+immediately aborts.
 .Ss Including other source files
 Use
 .Ic INCLUDE
--- a/src/asm/warning.c
+++ b/src/asm/warning.c
@@ -36,6 +36,7 @@
 	WARNING_DISABLED, /* Obsolete things */
 	WARNING_DISABLED, /* Shifting undefined behavior */
 	WARNING_ENABLED,  /* User warnings */
+	WARNING_ENABLED,  /* Assertions */
 	WARNING_DISABLED, /* Strange shift amount */
 	WARNING_ENABLED,  /* Implicit truncation loses some bits */
 };
@@ -72,6 +73,7 @@
 	"obsolete",
 	"shift",
 	"user",
+	"assert",
 	"shift-amount",
 	"truncation",
 
--- a/src/link/object.c
+++ b/src/link/object.c
@@ -13,13 +13,15 @@
 #include <errno.h>
 #include <limits.h>
 
-#include "link/object.h"
+#include "link/assign.h"
 #include "link/main.h"
-#include "link/symbol.h"
+#include "link/object.h"
+#include "link/patch.h"
 #include "link/section.h"
-#include "link/assign.h"
+#include "link/symbol.h"
 
 #include "extern/err.h"
+#include "helpers.h"
 #include "linkdefs.h"
 
 static struct SymbolList {
@@ -28,6 +30,8 @@
 	struct SymbolList *next;
 } *symbolLists;
 
+static struct Assertion *assertions;
+
 /***** Helper functions for reading object files *****/
 
 /*
@@ -232,7 +236,7 @@
 }
 
 /**
- * Reads a RGB6 section from a file.
+ * Reads a section from a file.
  * @param file The file to read from
  * @param section The struct to fill
  * @param fileName The filename to report in errors
@@ -332,6 +336,29 @@
 }
 
 /**
+ * Reads an assertion from a file
+ * @param file The file to read from
+ * @param assert The struct to fill
+ * @param fileName The filename to report in errors
+ */
+static void readAssertion(FILE *file, struct Assertion *assert,
+			  char const *fileName, struct Section *fileSections[],
+			  uint32_t i)
+{
+	char assertName[sizeof("Assertion #" EXPAND_AND_STR(UINT32_MAX))];
+	uint32_t sectionID;
+
+	snprintf(assertName, sizeof(assertName), "Assertion #%u", i);
+
+	readPatch(file, &assert->patch, fileName, assertName, 0);
+	tryReadlong(sectionID, file, "%s: Cannot read assertion's section ID: %s",
+		    fileName);
+	assert->section = fileSections[sectionID];
+	tryReadstr(assert->message, file, "%s: Cannot read assertion's message: %s",
+		   fileName);
+}
+
+/**
  * Reads an object file of any supported format
  * @param fileName The filename to report for errors
  */
@@ -451,6 +478,21 @@
 		}
 	}
 
+	uint32_t nbAsserts;
+
+	tryReadlong(nbAsserts, file, "%s: Cannot read number of assertions: %s",
+		    fileName);
+	verbosePrint("Reading %u assertions...\n", nbAsserts);
+	for (uint32_t i = 0; i < nbAsserts; i++) {
+		struct Assertion *assertion = malloc(sizeof(*assertion));
+
+		if (!assertion)
+			err(1, "%s: Couldn't create new assertion", fileName);
+		readAssertion(file, assertion, fileName, fileSections, i);
+		assertion->next = assertions;
+		assertions = assertion;
+	}
+
 	fclose(file);
 }
 
@@ -457,6 +499,8 @@
 void obj_DoSanityChecks(void)
 {
 	sect_DoSanityChecks();
+
+	patch_CheckAssertions(assertions);
 }
 
 static void freeSection(struct Section *section, void *arg)
--- a/src/link/patch.c
+++ b/src/link/patch.c
@@ -340,6 +340,48 @@
 #undef popRPN
 }
 
+void patch_CheckAssertions(struct Assertion *assert)
+{
+	verbosePrint("Checking assertions...");
+	initRPNStack();
+
+	uint8_t failures = 0;
+
+	while (assert) {
+		if (!computeRPNExpr(&assert->patch, assert->section)) {
+			switch ((enum AssertionType)assert->patch.type) {
+			case ASSERT_FATAL:
+				errx(1, "%s: %s", assert->patch.fileName,
+				     assert->message[0] ? assert->message
+							: "assert failure");
+				/* Not reached */
+				break; /* Here so checkpatch doesn't complain */
+			case ASSERT_ERROR:
+				fprintf(stderr, "%s: %s\n",
+					assert->patch.fileName,
+					assert->message[0] ? assert->message
+							   : "assert failure");
+				failures++;
+				break;
+			case ASSERT_WARN:
+				warnx("%s: %s", assert->patch.fileName,
+				      assert->message[0] ? assert->message
+							 : "assert failure");
+				break;
+			}
+		}
+		struct Assertion *next = assert->next;
+
+		free(assert);
+		assert = next;
+	}
+
+	freeRPNStack();
+
+	if (failures)
+		errx(1, "%u assertions failed!", failures);
+}
+
 /**
  * Applies all of a section's patches
  * @param section The section to patch
@@ -399,3 +441,4 @@
 	sect_ForEach(applyPatches, NULL);
 	freeRPNStack();
 }
+
--- a/src/rgbds.5
+++ b/src/rgbds.5
@@ -124,6 +124,34 @@
     ENDC
 
 ENDR
+
+; Assertions
+
+LONG  NumberOfAssertions
+
+REPT  NumberOfAssertions
+
+  STRING  SourceFile   ; Name of the source file (for printing the failure).
+
+  LONG    Offset       ; Offset into the section where the assertion is located.
+
+  BYTE    Type         ; 0 = Prints the message but allows linking to continue
+                       ; 1 = Prints the message and evaluates other assertions,
+                       ;     but linking fails afterwards
+                       ; 2 = Prints the message and immediately fails linking
+
+  LONG    RPNSize      ; Size of the RPN expression's buffer.
+
+  BYTE    RPN[RPNSize] ; RPN expression, same as patches. Assert fails if == 0.
+
+  LONG    SectionID    ; The section number (of this object file) in which this
+                       ; assert is defined. If it doesn't belong to any specific
+                       ; section (like a constant), this field has the value -1.
+
+  STRING  Message      ; A message displayed when the assert fails. If set to
+                       ; the empty string, a generic message is printed instead.
+
+ENDR
 .Ed
 .Ss RPN DATA
 Expressions in the object file are stored as RPN.
--- /dev/null
+++ b/test/asm/assert.asm
@@ -1,0 +1,23 @@
+SECTION "fixed", ROM0[0]
+
+FixedBase:
+	assert FixedBase ; This should eval (and fail) at compile time
+
+	ds 0
+	static_assert @ == 0, "@ ain't 0 now? (Hint: it's {@})"
+
+	ds 42
+	assert WARN, @ - FixedBase != 42 ; This should also eval at compile time
+
+SECTION "floating", ROM0
+
+FloatingBase:
+	assert FAIL, FloatingBase == 0 ; This shouldn't eval at compile time
+
+	ds 4
+	static_assert FAIL, FloatingBase != 0 ; This is not constant!
+
+	ds 69
+	static_assert FATAL, FixedBase != 0 ; This will fail... ↓
+	; The point of `FATAL` is for stuff that can, say, cause division by 0!
+	static_assert FAIL, 1 / FixedBase, "You dun goofed, son" ; Won't be read
--- /dev/null
+++ b/test/asm/assert.err
@@ -1,0 +1,10 @@
+ERROR: assert.asm(4):
+    Assertion failed
+warning: assert.asm(10): [-Wassert]
+    Assertion failed
+ERROR: assert.asm(18):
+    Expected constant expression: 'FloatingBase' is not constant at assembly time
+ERROR: assert.asm(18):
+    Assertion failed
+ERROR: assert.asm(21):
+    Assertion failed
--- /dev/null
+++ b/test/link/assert.asm
@@ -1,0 +1,11 @@
+
+SECTION "test", ROM0
+
+	ds 123
+
+FloatingBase:
+	assert WARN, FloatingBase & 0, "Worry about me, but not too much."
+	assert FAIL, FloatingBase & 0, "Okay, this is getting serious!"
+	assert FATAL, FloatingBase & 0, "It all ends now."
+	assert FAIL, FloatingBase & 0, "Not even time to roll credits!"
+	assert WARN, 0, "Still can finish the film, though!"
--- /dev/null
+++ b/test/link/assert.out
@@ -1,0 +1,3 @@
+warning: assert.asm(7): Worry about me, but not too much.
+assert.asm(8): Okay, this is getting serious!
+error: assert.asm(9): It all ends now.