shithub: rgbds

Download patch

ref: 5a65188ca94f2ebaa6ffabac4e7d04cb166de4d6
parent: 930080f556fc5ac7c41a1e5e05e8ab01e1789142
author: ISSOtm <[email protected]>
date: Mon Sep 28 23:40:15 EDT 2020

Implement compact file stacks in object files

Gets rid of `open_memstream`, enabling Windows compatibility again
Also fixes #491 as a nice bonus!

--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -46,7 +46,7 @@
   add_definitions(/D_CRT_SECURE_NO_WARNINGS)
 else()
   if(DEVELOP)
-    add_compile_options(-Werror -Wall -Wextra -pedantic
+    add_compile_options(-Werror -Wall -Wextra -pedantic -Wno-type-limits
                         -Wno-sign-compare -Wformat -Wformat-security -Wformat-overflow=2
                         -Wformat-truncation=1 -Wformat-y2k -Wswitch-enum -Wunused
                         -Wuninitialized -Wunknown-pragmas -Wstrict-overflow=5
--- a/Makefile
+++ b/Makefile
@@ -186,7 +186,7 @@
 # compilation and make the continous integration infrastructure return failure.
 
 develop:
-	$Qenv $(MAKE) -j WARNFLAGS="-Werror -Wall -Wextra -Wpedantic \
+	$Qenv $(MAKE) -j WARNFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-type-limits \
 		-Wno-sign-compare -Wformat -Wformat-security -Wformat-overflow=2 \
 		-Wformat-truncation=1 -Wformat-y2k -Wswitch-enum -Wunused \
 		-Wuninitialized -Wunknown-pragmas -Wstrict-overflow=5 \
--- a/include/asm/fstack.h
+++ b/include/asm/fstack.h
@@ -21,26 +21,44 @@
 
 #include "types.h"
 
-struct MacroArgs;
+struct FileStackNode {
+	struct FileStackNode *parent; /* Pointer to parent node, for error reporting */
+	/* Line at which the parent context was exited; meaningless for the root level */
+	uint32_t lineNo;
 
-struct sContext {
-	struct LexerState *lexerState;
-	struct Symbol const *pMacro;
-	struct sContext *next;
-	char tzFileName[_MAX_PATH + 1];
-	struct MacroArgs *macroArgs;
-	uint32_t uniqueID;
-	int32_t nLine;
-	uint32_t nStatus;
-	char const *pREPTBlock;
-	uint32_t nREPTBlockCount;
-	uint32_t nREPTBlockSize;
-	int32_t nREPTBodyFirstLine;
-	int32_t nREPTBodyLastLine;
+	struct FileStackNode *next; /* Next node in the output linked list */
+	bool referenced; /* If referenced, don't free! */
+	uint32_t ID; /* Set only if referenced: ID within the object file, -1 if not output yet */
+
+	enum {
+		NODE_REPT,
+		NODE_FILE,
+		NODE_MACRO,
+	} type;
 };
 
+struct FileStackReptNode { /* NODE_REPT */
+	struct FileStackNode node;
+	uint32_t reptDepth;
+	/* WARNING: if changing this type, change overflow check in `fstk_Init` */
+	uint32_t iters[]; /* REPT iteration counts since last named node, in reverse depth order */
+};
+
+struct FileStackNamedNode { /* NODE_FILE, NODE_MACRO */
+	struct FileStackNode node;
+	char name[]; /* File name for files, file::macro name for macros */
+};
+
 extern size_t nMaxRecursionDepth;
 
+struct MacroArgs;
+
+void fstk_Dump(struct FileStackNode const *node, uint32_t lineNo);
+void fstk_DumpCurrent(void);
+struct FileStackNode *fstk_GetFileStack(void);
+/* The lifetime of the returned chars is until reaching the end of that file */
+char const *fstk_GetFileName(void);
+
 void fstk_AddIncludePath(char const *s);
 /**
  * @param path The user-provided file name
@@ -53,14 +71,9 @@
 
 bool yywrap(void);
 void fstk_RunInclude(char const *path);
-void fstk_RunMacro(char *macroName, struct MacroArgs *args);
+void fstk_RunMacro(char const *macroName, struct MacroArgs *args);
 void fstk_RunRept(uint32_t count, int32_t nReptLineNo, char *body, size_t size);
 
-void fstk_Dump(void);
-char *fstk_DumpToStr(void);
-char const *fstk_GetFileName(void);
-uint32_t fstk_GetLine(void);
-
-void fstk_Init(char *mainPath, size_t maxRecursionDepth);
+void fstk_Init(char const *mainPath, size_t maxRecursionDepth);
 
 #endif /* RGBDS_ASM_FSTACK_H */
--- a/include/asm/lexer.h
+++ b/include/asm/lexer.h
@@ -43,6 +43,9 @@
 	gfxDigits = digits;
 }
 
+/*
+ * `path` is referenced, but not held onto..!
+ */
 struct LexerState *lexer_OpenFile(char const *path);
 struct LexerState *lexer_OpenFileView(char *buf, size_t size, uint32_t lineNo);
 void lexer_RestartRept(uint32_t lineNo);
--- a/include/asm/output.h
+++ b/include/asm/output.h
@@ -18,6 +18,8 @@
 extern char *tzObjectname;
 extern struct Section *pSectionList, *pCurrentSection;
 
+void out_RegisterNode(struct FileStackNode *node);
+void out_ReplaceNode(struct FileStackNode *node);
 void out_SetFileName(char *s);
 void out_CreatePatch(uint32_t type, struct Expression const *expr,
 		     uint32_t ofs);
--- a/include/asm/symbol.h
+++ b/include/asm/symbol.h
@@ -35,8 +35,8 @@
 	bool isExported; /* Whether the symbol is to be exported */
 	bool isBuiltin;  /* Whether the symbol is a built-in */
 	struct Section *section;
-	char fileName[_MAX_PATH + 1]; /* File where the symbol was defined. */
-	uint32_t fileLine; /* Line where the symbol was defined. */
+	struct FileStackNode *src; /* Where the symbol was defined */
+	uint32_t fileLine; /* Line where the symbol was defined */
 
 	bool hasCallback;
 	union {
--- a/include/link/main.h
+++ b/include/link/main.h
@@ -29,6 +29,25 @@
 extern bool isWRA0Mode;
 extern bool disablePadding;
 
+struct FileStackNode {
+	struct FileStackNode *parent;
+	/* Line at which the parent context was exited; meaningless for the root level */
+	uint32_t lineNo;
+
+	enum {
+		NODE_REPT,
+		NODE_FILE,
+		NODE_MACRO,
+	} type;
+	union {
+		char *name; /* NODE_FILE, NODE_MACRO */
+		struct { /* NODE_REPT */
+			uint32_t reptDepth;
+			uint32_t *iters;
+		};
+	};
+};
+
 /* Helper macro for printing verbose-mode messages */
 #define verbosePrint(...)   do { \
 					if (beVerbose) \
@@ -35,9 +54,20 @@
 						fprintf(stderr, __VA_ARGS__); \
 				} while (0)
 
-void error(char const *fmt, ...);
+/**
+ * Dump a file stack to stderr
+ * @param node The leaf node to dump the context of
+ */
+char const *dumpFileStack(struct FileStackNode const *node);
 
-noreturn_ void fatal(char const *fmt, ...);
+void warning(struct FileStackNode const *where, uint32_t lineNo,
+	     char const *fmt, ...) format_(printf, 3, 4);
+
+void error(struct FileStackNode const *where, uint32_t lineNo,
+	   char const *fmt, ...) format_(printf, 3, 4);
+
+noreturn_ void fatal(struct FileStackNode const *where, uint32_t lineNo,
+		     char const *fmt, ...) format_(printf, 3, 4);
 
 /**
  * Opens a file if specified, and aborts on error.
--- a/include/link/object.h
+++ b/include/link/object.h
@@ -14,8 +14,9 @@
 /**
  * Read an object (.o) file, and add its info to the data structures.
  * @param fileName A path to the object file to be read
+ * @param i The ID of the file
  */
-void obj_ReadFile(char const *fileName);
+void obj_ReadFile(char const *fileName, unsigned int i);
 
 /**
  * Perform validation on the object files' contents
@@ -26,6 +27,12 @@
  * Evaluate all assertions
  */
 void obj_CheckAssertions(void);
+
+/**
+ * Sets up object file reading
+ * @param nbFiles The number of object files that will be read
+ */
+void obj_Setup(unsigned int nbFiles);
 
 /**
  * `free`s all object memory that was allocated.
--- a/include/link/section.h
+++ b/include/link/section.h
@@ -19,6 +19,7 @@
 
 #include "linkdefs.h"
 
+struct FileStackNode;
 struct Section;
 
 struct AttachedSymbol {
@@ -27,7 +28,8 @@
 };
 
 struct Patch {
-	char *fileName;
+	struct FileStackNode const *src;
+	uint32_t lineNo;
 	int32_t offset;
 	uint32_t pcSectionID;
 	uint32_t pcOffset;
--- a/include/link/symbol.h
+++ b/include/link/symbol.h
@@ -16,12 +16,14 @@
 
 #include "linkdefs.h"
 
+struct FileStackNode;
+
 struct Symbol {
 	/* Info contained in the object files */
 	char *name;
 	enum ExportLevel type;
 	char const *objFileName;
-	char *fileName;
+	struct FileStackNode const *src;
 	int32_t lineNo;
 	int32_t sectionID;
 	union {
--- a/include/linkdefs.h
+++ b/include/linkdefs.h
@@ -14,7 +14,7 @@
 
 #define RGBDS_OBJECT_VERSION_STRING "RGB%1u"
 #define RGBDS_OBJECT_VERSION_NUMBER 9U
-#define RGBDS_OBJECT_REV 5U
+#define RGBDS_OBJECT_REV 6U
 
 enum AssertionType {
 	ASSERT_WARN,
--- a/src/asm/fstack.c
+++ b/src/asm/fstack.c
@@ -29,27 +29,83 @@
 
 struct Context {
 	struct Context *parent;
-	struct Context *child;
+	struct FileStackNode *fileInfo;
 	struct LexerState *lexerState;
 	uint32_t uniqueID;
-	char const *fileName;
-	char *fileNameBuf;
-	uint32_t lineNo; /* Line number at which the context was EXITED */
-	struct Symbol const *macro;
 	struct MacroArgs *macroArgs; /* Macro args are *saved* here */
-	uint32_t nbReptIters; /* If zero, this isn't a REPT block */
-	size_t reptDepth;
-	uint32_t reptIters[];
+	uint32_t nbReptIters;
 };
 
 static struct Context *contextStack;
-static struct Context *topLevelContext;
 static size_t contextDepth = 0;
+#define DEFAULT_MAX_DEPTH 64
 size_t nMaxRecursionDepth;
 
 static unsigned int nbIncPaths = 0;
 static char const *includePaths[MAXINCPATHS];
 
+char const *dumpNodeAndParents(struct FileStackNode const *node)
+{
+	char const *name;
+
+	if (node->type == NODE_REPT) {
+		assert(node->parent); /* REPT nodes should always have a parent */
+		struct FileStackReptNode const *reptInfo = (struct FileStackReptNode const *)node;
+
+		name = dumpNodeAndParents(node->parent);
+		fprintf(stderr, "(%" PRIu32 ") -> %s", node->lineNo, name);
+		for (uint32_t i = reptInfo->reptDepth; i--; )
+			fprintf(stderr, "::REPT~%" PRIu32, reptInfo->iters[i]);
+	} else {
+		name = ((struct FileStackNamedNode const *)node)->name;
+		if (node->parent) {
+			dumpNodeAndParents(node->parent);
+			fprintf(stderr, "(%" PRIu32 ") -> %s", node->lineNo, name);
+		} else {
+			fputs(name, stderr);
+		}
+	}
+	return name;
+}
+
+void fstk_Dump(struct FileStackNode const *node, uint32_t lineNo)
+{
+	dumpNodeAndParents(node);
+	fprintf(stderr, "(%" PRIu32 ")", lineNo);
+}
+
+void fstk_DumpCurrent(void)
+{
+	if (!contextStack) {
+		fputs("at top level", stderr);
+		return;
+	}
+	fstk_Dump(contextStack->fileInfo, lexer_GetLineNo());
+}
+
+struct FileStackNode *fstk_GetFileStack(void)
+{
+	struct FileStackNode *node = contextStack->fileInfo;
+
+	/* Mark node and all of its parents as referenced if not already so they don't get freed */
+	while (node && !node->referenced) {
+		node->ID = -1;
+		node->referenced = true;
+		node = node->parent;
+	}
+	return contextStack->fileInfo;
+}
+
+char const *fstk_GetFileName(void)
+{
+	/* Iterating via the nodes themselves skips nested REPTs */
+	struct FileStackNode const *node = contextStack->fileInfo;
+
+	while (node->type != NODE_FILE)
+		node = node->parent;
+	return ((struct FileStackNamedNode const *)node)->name;
+}
+
 void fstk_AddIncludePath(char const *path)
 {
 	if (path[0] == '\0')
@@ -141,12 +197,28 @@
 
 bool yywrap(void)
 {
-	if (contextStack->nbReptIters) { /* The context is a REPT block, which may loop */
-		contextStack->reptIters[contextStack->reptDepth - 1]++;
+	if (contextStack->fileInfo->type == NODE_REPT) { /* The context is a REPT block, which may loop */
+		struct FileStackReptNode *fileInfo = (struct FileStackReptNode *)contextStack->fileInfo;
+
+		/* If the node is referenced, we can't edit it; duplicate it */
+		if (contextStack->fileInfo->referenced) {
+			struct FileStackReptNode *copy = malloc(sizeof(*copy) + sizeof(copy->iters[0]) * fileInfo->reptDepth);
+
+			if (!copy)
+				fatalerror("Failed to duplicate REPT file node: %s\n", strerror(errno));
+			/* Copy all info but the referencing */
+			*copy = *fileInfo;
+			copy->node.next = NULL;
+			copy->node.referenced = false;
+
+			fileInfo = copy;
+			contextStack->fileInfo = (struct FileStackNode *)fileInfo;
+		}
+
+		fileInfo->iters[0]++;
 		/* If this wasn't the last iteration, wrap instead of popping */
-		if (contextStack->reptIters[contextStack->reptDepth - 1]
-								<= contextStack->nbReptIters) {
-			lexer_RestartRept(contextStack->parent->lineNo);
+		if (fileInfo->iters[0] <= contextStack->nbReptIters) {
+			lexer_RestartRept(contextStack->fileInfo->lineNo);
 			contextStack->uniqueID = macro_UseNewUniqueID();
 			return false;
 		}
@@ -155,44 +227,52 @@
 	}
 	dbgPrint("Popping context\n");
 
-	/* Free an `INCLUDE`'s path */
-	if (contextStack->fileNameBuf)
-		free(contextStack->fileNameBuf);
+	struct Context *context = contextStack;
 
 	contextStack = contextStack->parent;
 	contextDepth--;
 
-	lexer_DeleteState(contextStack->child->lexerState);
+	lexer_DeleteState(context->lexerState);
 	/* Restore args if a macro (not REPT) saved them */
-	if (contextStack->child->nbReptIters == 0 && contextStack->child->macro) {
+	if (context->fileInfo->type == NODE_MACRO) {
 		dbgPrint("Restoring macro args %p\n", contextStack->macroArgs);
 		macro_UseNewArgs(contextStack->macroArgs);
 	}
+	/* Free the file stack node */
+	if (!context->fileInfo->referenced)
+		free(context->fileInfo);
 	/* Free the entry and make its parent the current entry */
-	free(contextStack->child);
+	free(context);
 
-	contextStack->child = NULL;
 	lexer_SetState(contextStack->lexerState);
 	macro_SetUniqueID(contextStack->uniqueID);
 	return false;
 }
 
-static void newContext(uint32_t reptDepth)
+/*
+ * Make sure not to switch the lexer state before calling this, so the saved line no is correct
+ * BE CAREFUL!! This modifies the file stack directly, you should have set up the file info first
+ */
+static void newContext(struct FileStackNode *fileInfo)
 {
 	if (++contextDepth >= nMaxRecursionDepth)
 		fatalerror("Recursion limit (%zu) exceeded\n", nMaxRecursionDepth);
-	contextStack->child = malloc(sizeof(*contextStack->child)
-						+ reptDepth * sizeof(contextStack->reptIters[0]));
-	if (!contextStack->child)
+	struct Context *context = malloc(sizeof(*context));
+
+	if (!context)
 		fatalerror("Failed to allocate memory for new context: %s\n", strerror(errno));
+	fileInfo->parent = contextStack->fileInfo;
+	fileInfo->lineNo = 0; /* Init to a default value, see struct definition for info */
+	fileInfo->referenced = false;
+	fileInfo->lineNo = lexer_GetLineNo();
+	context->fileInfo = fileInfo;
+	/*
+	 * Link new entry to its parent so it's reachable later
+	 * ERRORS SHOULD NOT OCCUR AFTER THIS!!
+	 */
+	context->parent = contextStack;
+	contextStack = context;
 
-	contextStack->lineNo = lexer_GetLineNo();
-	/* Link new entry to its parent so it's reachable later */
-	contextStack->child->parent = contextStack;
-	contextStack = contextStack->child;
-
-	contextStack->child = NULL;
-	contextStack->reptDepth = reptDepth;
 }
 
 void fstk_RunInclude(char const *path)
@@ -212,8 +292,18 @@
 	}
 	dbgPrint("Full path: \"%s\"\n", fullPath);
 
-	newContext(0);
-	contextStack->lexerState = lexer_OpenFile(fullPath);
+	struct FileStackNamedNode *fileInfo = malloc(sizeof(*fileInfo) + size);
+
+	if (!fileInfo) {
+		error("Failed to alloc file info for INCLUDE: %s\n", strerror(errno));
+		return;
+	}
+	fileInfo->node.type = NODE_FILE;
+	strcpy(fileInfo->name, fullPath);
+	free(fullPath);
+
+	newContext((struct FileStackNode *)fileInfo);
+	contextStack->lexerState = lexer_OpenFile(fileInfo->name);
 	if (!contextStack->lexerState)
 		fatalerror("Failed to set up lexer for file include\n");
 	lexer_SetStateAtEOL(contextStack->lexerState);
@@ -220,13 +310,9 @@
 	/* We're back at top-level, so most things are reset */
 	contextStack->uniqueID = 0;
 	macro_SetUniqueID(0);
-	contextStack->fileName = fullPath;
-	contextStack->fileNameBuf = fullPath;
-	contextStack->macro = NULL;
-	contextStack->nbReptIters = 0;
 }
 
-void fstk_RunMacro(char *macroName, struct MacroArgs *args)
+void fstk_RunMacro(char const *macroName, struct MacroArgs *args)
 {
 	dbgPrint("Running macro \"%s\"\n", macroName);
 
@@ -242,7 +328,53 @@
 	}
 	contextStack->macroArgs = macro_GetCurrentArgs();
 
-	newContext(0);
+	/* Compute total length of this node's name: <base name>::<macro> */
+	size_t reptNameLen = 0;
+	struct FileStackNode const *node = macro->src;
+
+	if (node->type == NODE_REPT) {
+		struct FileStackReptNode const *reptNode = (struct FileStackReptNode const *)node;
+
+		/* 4294967295 = 2^32 - 1, aka UINT32_MAX */
+		reptNameLen += reptNode->reptDepth * strlen("::REPT~4294967295");
+		/* Look for next named node */
+		do {
+			node = node->parent;
+		} while (node->type == NODE_REPT);
+	}
+	struct FileStackNamedNode const *baseNode = (struct FileStackNamedNode const *)node;
+	size_t baseLen = strlen(baseNode->name);
+	size_t macroNameLen = strlen(macro->name);
+	struct FileStackNamedNode *fileInfo = malloc(sizeof(*fileInfo) + baseLen
+						     + reptNameLen + 2 + macroNameLen + 1);
+
+	if (!fileInfo) {
+		error("Failed to alloc file info for \"%s\": %s\n", macro->name, strerror(errno));
+		return;
+	}
+	fileInfo->node.type = NODE_MACRO;
+	/* Print the name... */
+	char *dest = fileInfo->name;
+
+	memcpy(dest, baseNode->name, baseLen);
+	dest += baseLen;
+	if (node->type == NODE_REPT) {
+		struct FileStackReptNode const *reptNode = (struct FileStackReptNode const *)node;
+
+		for (uint32_t i = reptNode->reptDepth; i--; ) {
+			int nbChars = sprintf(dest, "::REPT~%" PRIu32, reptNode->iters[i]);
+
+			if (nbChars < 0)
+				fatalerror("Failed to write macro invocation info: %s\n",
+					   strerror(errno));
+			dest += nbChars;
+		}
+	}
+	*dest++ = ':';
+	*dest++ = ':';
+	memcpy(dest, macro->name, macroNameLen + 1);
+
+	newContext((struct FileStackNode *)fileInfo);
 	/* Line minus 1 because buffer begins with a newline */
 	contextStack->lexerState = lexer_OpenFileView(macro->macro, macro->macroSize,
 						      macro->fileLine - 1);
@@ -250,143 +382,93 @@
 		fatalerror("Failed to set up lexer for macro invocation\n");
 	lexer_SetStateAtEOL(contextStack->lexerState);
 	contextStack->uniqueID = macro_UseNewUniqueID();
-	contextStack->fileName = macro->fileName;
-	contextStack->fileNameBuf = NULL;
-	contextStack->macro = macro;
-	contextStack->nbReptIters = 0;
 	macro_UseNewArgs(args);
 }
 
-void fstk_RunRept(uint32_t count, int32_t nReptLineNo, char *body, size_t size)
+void fstk_RunRept(uint32_t count, int32_t reptLineNo, char *body, size_t size)
 {
 	dbgPrint("Running REPT(%" PRIu32 ")\n", count);
-
 	if (count == 0)
 		return;
-	uint32_t reptDepth = contextStack->reptDepth;
 
-	newContext(reptDepth + 1);
-	contextStack->lexerState = lexer_OpenFileView(body, size, nReptLineNo);
+	uint32_t reptDepth = contextStack->fileInfo->type == NODE_REPT
+				? ((struct FileStackReptNode *)contextStack->fileInfo)->reptDepth
+				: 0;
+	struct FileStackReptNode *fileInfo = malloc(sizeof(*fileInfo)
+						    + (reptDepth + 1) * sizeof(fileInfo->iters[0]));
+
+	if (!fileInfo) {
+		error("Failed to alloc file info for REPT: %s\n", strerror(errno));
+		return;
+	}
+	fileInfo->node.type = NODE_REPT;
+	fileInfo->reptDepth = reptDepth + 1;
+	fileInfo->iters[0] = 1;
+	if (reptDepth)
+		/* Copy all parent iter counts */
+		memcpy(&fileInfo->iters[1],
+		       ((struct FileStackReptNode *)contextStack->fileInfo)->iters,
+		       reptDepth * sizeof(fileInfo->iters[0]));
+
+	newContext((struct FileStackNode *)fileInfo);
+	/* Correct our line number, which currently points to the `ENDR` line */
+	contextStack->fileInfo->lineNo = reptLineNo;
+
+	contextStack->lexerState = lexer_OpenFileView(body, size, reptLineNo);
 	if (!contextStack->lexerState)
-		fatalerror("Failed to set up lexer for macro invocation\n");
+		fatalerror("Failed to set up lexer for rept block\n");
 	lexer_SetStateAtEOL(contextStack->lexerState);
 	contextStack->uniqueID = macro_UseNewUniqueID();
-	contextStack->fileName = contextStack->parent->fileName;
-	contextStack->fileNameBuf = NULL;
-	contextStack->macro = contextStack->parent->macro; /* Inherit */
 	contextStack->nbReptIters = count;
-	/* Copy all of parent's iters, and add ours */
-	if (reptDepth)
-		memcpy(contextStack->reptIters, contextStack->parent->reptIters,
-		       sizeof(contextStack->reptIters[0]) * reptDepth);
-	contextStack->reptIters[reptDepth] = 1;
 
-	/* Correct our parent's line number, which currently points to the `ENDR` line */
-	contextStack->parent->lineNo = nReptLineNo;
 }
 
-static void printContext(FILE *stream, struct Context const *context)
+void fstk_Init(char const *mainPath, size_t maxRecursionDepth)
 {
-	fprintf(stream, "%s", context->fileName);
-	if (context->macro)
-		fprintf(stream, "::%s", context->macro->name);
-	for (size_t i = 0; i < context->reptDepth; i++)
-		fprintf(stream, "::REPT~%" PRIu32, context->reptIters[i]);
-	fprintf(stream, "(%" PRId32 ")", context->lineNo);
-}
+	struct LexerState *state = lexer_OpenFile(mainPath);
 
-static void dumpToStream(FILE *stream)
-{
-	struct Context *context = topLevelContext;
+	if (!state)
+		fatalerror("Failed to open main file!\n");
+	lexer_SetState(state);
+	char const *fileName = lexer_GetFileName();
+	size_t len = strlen(fileName);
+	struct Context *context = malloc(sizeof(*contextStack));
+	struct FileStackNamedNode *fileInfo = malloc(sizeof(*fileInfo) + len + 1);
 
-	while (context != contextStack) {
-		printContext(stream, context);
-		fprintf(stream, " -> ");
-		context = context->child;
-	}
-	contextStack->lineNo = lexer_GetLineNo();
-	printContext(stream, contextStack);
-}
+	if (!context)
+		fatalerror("Failed to allocate memory for main context: %s\n", strerror(errno));
+	if (!fileInfo)
+		fatalerror("Failed to allocate memory for main file info: %s\n", strerror(errno));
 
-void fstk_Dump(void)
-{
-	dumpToStream(stderr);
-}
+	context->fileInfo = (struct FileStackNode *)fileInfo;
+	/* lineNo and reptIter are unused on the top-level context */
+	context->fileInfo->parent = NULL;
+	context->fileInfo->referenced = false;
+	context->fileInfo->type = NODE_FILE;
+	memcpy(fileInfo->name, fileName, len + 1);
 
-char *fstk_DumpToStr(void)
-{
-	char *str;
-	size_t size;
-	/* `open_memstream` is specified to always include a '\0' at the end of the buffer! */
-	FILE *stream = open_memstream(&str, &size);
-
-	if (!stream)
-		fatalerror("Failed to dump file stack to string: %s\n", strerror(errno));
-	dumpToStream(stream);
-	fclose(stream);
-	return str;
-}
-
-char const *fstk_GetFileName(void)
-{
-	/* FIXME: this is awful, but all callees copy the buffer anyways */
-	static char fileName[_MAX_PATH + 1];
-	size_t remainingChars = _MAX_PATH + 1;
-	char *dest = fileName;
-	char const *src = contextStack->fileName;
-
-#define append(...) do { \
-	int nbChars = snprintf(dest, remainingChars, __VA_ARGS__); \
-	\
-	if (nbChars >= remainingChars) \
-		fatalerror("File stack entry too large\n"); \
-	remainingChars -= nbChars; \
-	dest += nbChars; \
-} while (0)
-
-	while (*src && --remainingChars) /* Leave room for terminator */
-		*dest++ = *src++;
-	if (remainingChars && contextStack->macro)
-		append("::%s", contextStack->macro->name);
-	for (size_t i = 0; i < contextStack->reptDepth; i++)
-		append("::REPT~%" PRIu32, contextStack->reptIters[i]);
-
-	*dest = '\0';
-	return fileName;
-}
-
-uint32_t fstk_GetLine(void)
-{
-	return lexer_GetLineNo();
-}
-
-void fstk_Init(char *mainPath, size_t maxRecursionDepth)
-{
-	topLevelContext = malloc(sizeof(*topLevelContext));
-	if (!topLevelContext)
-		fatalerror("Failed to allocate memory for initial context: %s\n", strerror(errno));
-	topLevelContext->parent = NULL;
-	topLevelContext->child = NULL;
-	topLevelContext->lexerState = lexer_OpenFile(mainPath);
-	if (!topLevelContext->lexerState)
-		fatalerror("Failed to open main file!\n");
-	lexer_SetState(topLevelContext->lexerState);
-	topLevelContext->uniqueID = 0;
+	context->parent = NULL;
+	context->lexerState = state;
+	context->uniqueID = 0;
 	macro_SetUniqueID(0);
-	topLevelContext->fileName = lexer_GetFileName();
-	topLevelContext->fileNameBuf = NULL;
-	topLevelContext->macro = NULL;
-	topLevelContext->nbReptIters = 0;
-	topLevelContext->reptDepth = 0;
+	context->nbReptIters = 0;
 
-	contextStack = topLevelContext;
+	/* Now that it's set up properly, register the context */
+	contextStack = context;
 
-	if (maxRecursionDepth
-			> (SIZE_MAX - sizeof(*contextStack)) / sizeof(contextStack->reptIters[0])) {
-		error("Recursion depth may not be higher than %zu, defaulting to 64\n",
-		      (SIZE_MAX - sizeof(*contextStack)) / sizeof(contextStack->reptIters[0]));
-		nMaxRecursionDepth = 64;
+	/*
+	 * Check that max recursion depth won't allow overflowing node `malloc`s
+	 * This assumes that the rept node is larger
+	 */
+#define DEPTH_LIMIT ((SIZE_MAX - sizeof(struct FileStackReptNode)) / sizeof(uint32_t))
+	if (maxRecursionDepth > DEPTH_LIMIT) {
+		error("Recursion depth may not be higher than %zu, defaulting to "
+		      EXPAND_AND_STR(DEFAULT_MAX_DEPTH) "\n", DEPTH_LIMIT);
+		nMaxRecursionDepth = DEFAULT_MAX_DEPTH;
 	} else {
 		nMaxRecursionDepth = maxRecursionDepth;
 	}
+	/* Make sure that the default of 64 is OK, though */
+	assert(DEPTH_LIMIT >= DEFAULT_MAX_DEPTH);
+#undef DEPTH_LIMIT
 }
--- a/src/asm/output.c
+++ b/src/asm/output.c
@@ -12,6 +12,7 @@
 
 #include <assert.h>
 #include <errno.h>
+#include <inttypes.h>
 #include <stdio.h>
 #include <stdint.h>
 #include <stdlib.h>
@@ -33,7 +34,8 @@
 #include "platform.h" // strdup
 
 struct Patch {
-	char *tzFilename;
+	struct FileStackNode const *src;
+	uint32_t lineNo;
 	uint32_t nOffset;
 	struct Section *pcSection;
 	uint32_t pcOffset;
@@ -62,19 +64,17 @@
 
 static struct Assertion *assertions = NULL;
 
+static struct FileStackNode *fileStackNodes = NULL;
+
 /*
  * Count the number of sections used in this object
  */
 static uint32_t countsections(void)
 {
-	struct Section *sect;
 	uint32_t count = 0;
 
-	sect = pSectionList;
-	while (sect) {
+	for (struct Section const *sect = pSectionList; sect; sect = sect->next)
 		count++;
-		sect = sect->next;
-	}
 
 	return count;
 }
@@ -129,16 +129,60 @@
 	fputc(0, f);
 }
 
+static uint32_t getNbFileStackNodes(void)
+{
+	return fileStackNodes ? fileStackNodes->ID + 1 : 0;
+}
+
+void out_RegisterNode(struct FileStackNode *node)
+{
+	/* If node is not already registered, register it (and parents), and give it a unique ID */
+	while (node->ID == -1) {
+		node->ID = getNbFileStackNodes();
+		if (node->ID == -1)
+			fatalerror("Reached too many file stack nodes; try splitting the file up\n");
+		node->next = fileStackNodes;
+		fileStackNodes = node;
+
+		/* Also register the node's parents */
+		node = node->parent;
+		if (!node)
+			break;
+	}
+}
+
+void out_ReplaceNode(struct FileStackNode *node)
+{
+	(void)node;
+#if 0
+This is code intended to replace a node, which is pretty useless until ref counting is added...
+
+	struct FileStackNode **ptr = &fileStackNodes;
+
+	/*
+	 * The linked list is supposed to have decrementing IDs, so iterate with less memory reads,
+	 * to hopefully hit the cache less. A debug check is added after, in case a change is made
+	 * that breaks this assumption.
+	 */
+	for (uint32_t i = fileStackNodes->ID; i != node->ID; i--)
+		ptr = &(*ptr)->next;
+	assert((*ptr)->ID == node->ID);
+
+	node->next = (*ptr)->next;
+	assert(!node->next || node->next->ID == node->ID - 1); /* Catch inconsistencies early */
+	/* TODO: unreference the node */
+	*ptr = node;
+#endif
+}
+
 /*
  * Return a section's ID
  */
 static uint32_t getsectid(struct Section const *sect)
 {
-	struct Section const *sec;
+	struct Section const *sec = pSectionList;
 	uint32_t ID = 0;
 
-	sec = pSectionList;
-
 	while (sec) {
 		if (sec == sect)
 			return ID;
@@ -159,7 +203,10 @@
  */
 static void writepatch(struct Patch const *patch, FILE *f)
 {
-	fputstring(patch->tzFilename, f);
+	assert(patch->src->ID != -1);
+
+	fputlong(patch->src->ID, f);
+	fputlong(patch->lineNo, f);
 	fputlong(patch->nOffset, f);
 	fputlong(getSectIDIfAny(patch->pcSection), f);
 	fputlong(patch->pcOffset, f);
@@ -206,8 +253,10 @@
 	if (!sym_IsDefined(sym)) {
 		fputc(SYMTYPE_IMPORT, f);
 	} else {
+		assert(sym->src->ID != -1);
+
 		fputc(sym->isExported ? SYMTYPE_EXPORT : SYMTYPE_LOCAL, f);
-		fputstring(sym->fileName, f);
+		fputlong(sym->src->ID, f);
 		fputlong(sym->fileLine, f);
 		fputlong(getSectIDIfAny(sym_GetSection(sym)), f);
 		fputlong(sym->value, f);
@@ -214,6 +263,17 @@
 	}
 }
 
+static void registerSymbol(struct Symbol *sym)
+{
+	*objectSymbolsTail = sym;
+	objectSymbolsTail = &sym->next;
+	out_RegisterNode(sym->src);
+	if (nbSymbols == -1)
+		fatalerror("Registered too many symbols (%" PRIu32
+			   "); try splitting up your files\n", (uint32_t)-1);
+	sym->ID = nbSymbols++;
+}
+
 /*
  * Returns a symbol's ID within the object file
  * If the symbol does not have one, one is assigned by registering the symbol
@@ -220,12 +280,8 @@
  */
 static uint32_t getSymbolID(struct Symbol *sym)
 {
-	if (sym->ID == -1) {
-		sym->ID = nbSymbols++;
-
-		*objectSymbolsTail = sym;
-		objectSymbolsTail = &sym->next;
-	}
+	if (sym->ID == -1 && !sym_IsPC(sym))
+		registerSymbol(sym);
 	return sym->ID;
 }
 
@@ -303,22 +359,25 @@
 
 /*
  * Allocate a new patch structure and link it into the list
+ * WARNING: all patches are assumed to eventually be written, so the file stack node is registered
  */
-static struct Patch *allocpatch(uint32_t type, struct Expression const *expr,
-				uint32_t ofs)
+static struct Patch *allocpatch(uint32_t type, struct Expression const *expr, uint32_t ofs)
 {
 	struct Patch *patch = malloc(sizeof(struct Patch));
 	uint32_t rpnSize = expr->isKnown ? 5 : expr->nRPNPatchSize;
+	struct FileStackNode *node = fstk_GetFileStack();
 
 	if (!patch)
 		fatalerror("No memory for patch: %s\n", strerror(errno));
-	patch->pRPN = malloc(sizeof(*patch->pRPN) * rpnSize);
 
+	patch->pRPN = malloc(sizeof(*patch->pRPN) * rpnSize);
 	if (!patch->pRPN)
 		fatalerror("No memory for patch's RPN expression: %s\n", strerror(errno));
 
 	patch->type = type;
-	patch->tzFilename = fstk_DumpToStr();
+	patch->src = node;
+	out_RegisterNode(node);
+	patch->lineNo = lexer_GetLineNo();
 	patch->nOffset = ofs;
 	patch->pcSection = sect_GetSymbolSection();
 	patch->pcOffset = sect_GetSymbolOffset();
@@ -382,13 +441,28 @@
 	fputstring(assert->message, f);
 }
 
+static void writeFileStackNode(struct FileStackNode const *node, FILE *f)
+{
+	fputlong(node->parent ? node->parent->ID : -1, f);
+	fputlong(node->lineNo, f);
+	fputc(node->type, f);
+	if (node->type != NODE_REPT) {
+		fputstring(((struct FileStackNamedNode const *)node)->name, f);
+	} else {
+		struct FileStackReptNode const *reptNode = (struct FileStackReptNode const *)node;
+
+		fputlong(reptNode->reptDepth, f);
+		/* Iters are stored by decreasing depth, so reverse the order for output */
+		for (uint32_t i = reptNode->reptDepth; i--; )
+			fputlong(reptNode->iters[i], f);
+	}
+}
+
 static void registerExportedSymbol(struct Symbol *symbol, void *arg)
 {
 	(void)arg;
 	if (sym_IsExported(symbol) && symbol->ID == -1) {
-		*objectSymbolsTail = symbol;
-		objectSymbolsTail = &symbol->next;
-		nbSymbols++;
+		registerSymbol(symbol);
 	}
 }
 
@@ -410,6 +484,15 @@
 
 	fputlong(nbSymbols, f);
 	fputlong(countsections(), f);
+
+	fputlong(getNbFileStackNodes(), f);
+	for (struct FileStackNode const *node = fileStackNodes; node; node = node->next) {
+		writeFileStackNode(node, f);
+		if (node->next && node->next->ID != node->ID - 1)
+			fatalerror("Internal error: fstack node #%" PRIu32 " follows #%" PRIu32
+				   ". Please report this to the developers!\n",
+				   node->next->ID, node->ID);
+	}
 
 	for (struct Symbol const *sym = objectSymbols; sym; sym = sym->next)
 		writesymbol(sym, f);
--- a/src/asm/rpn.c
+++ b/src/asm/rpn.c
@@ -258,8 +258,8 @@
 	if (amount >= 0) {
 		// Left shift
 		if (amount >= 32) {
-			warning(WARNING_SHIFT_AMOUNT, "Shifting left by large amount %" PRId32 "\n",
-				amount);
+			warning(WARNING_SHIFT_AMOUNT, "Shifting left by large amount %"
+				PRId32 "\n", amount);
 			return 0;
 
 		} else {
--- a/src/asm/symbol.c
+++ b/src/asm/symbol.c
@@ -23,6 +23,7 @@
 #include "asm/macro.h"
 #include "asm/main.h"
 #include "asm/mymath.h"
+#include "asm/output.h"
 #include "asm/section.h"
 #include "asm/symbol.h"
 #include "asm/util.h"
@@ -121,7 +122,7 @@
 			buf[j - 1] = '\\';
 		buf[j] = fileName[i];
 	}
-	/* Write everything after the loop, to ensure everything has been allocated */
+	/* Write everything after the loop, to ensure the buffer has been allocated */
 	buf[0] = '"';
 	buf[j++] = '"';
 	buf[j] = '\0';
@@ -150,15 +151,35 @@
 	return sym->value;
 }
 
+static void dumpFilename(struct Symbol const *sym)
+{
+	if (!sym->src)
+		fputs("<builtin>", stderr);
+	else
+		fstk_Dump(sym->src, sym->fileLine);
+}
+
 /*
+ * Set a symbol's definition filename and line
+ */
+static void setSymbolFilename(struct Symbol *sym)
+{
+	sym->src = fstk_GetFileStack();
+	sym->fileLine = lexer_GetLineNo();
+}
+
+/*
  * Update a symbol's definition filename and line
  */
 static void updateSymbolFilename(struct Symbol *sym)
 {
-	if (snprintf(sym->fileName, _MAX_PATH + 1, "%s",
-		     fstk_GetFileName()) > _MAX_PATH)
-		fatalerror("%s: File name is too long: '%s'\n", __func__, fstk_GetFileName());
-	sym->fileLine = fstk_GetLine();
+	struct FileStackNode *oldSrc = sym->src;
+
+	setSymbolFilename(sym);
+	/* If the old node was referenced, ensure the new one is */
+	if (oldSrc->referenced && oldSrc->ID != -1)
+		out_RegisterNode(sym->src);
+	/* TODO: unref the old node, and use `out_ReplaceNode` instead if deleting it */
 }
 
 /*
@@ -178,7 +199,7 @@
 	symbol->isBuiltin = false;
 	symbol->hasCallback = false;
 	symbol->section = NULL;
-	updateSymbolFilename(symbol);
+	setSymbolFilename(symbol);
 	symbol->ID = -1;
 	symbol->next = NULL;
 
@@ -253,6 +274,7 @@
 			labelScope = NULL;
 
 		hash_RemoveElement(symbols, symbol->name);
+		/* TODO: ideally, also unref the file stack nodes */
 		free(symbol);
 	}
 }
@@ -338,9 +360,11 @@
 
 	if (!symbol)
 		symbol = createsymbol(symbolName);
-	else if (sym_IsDefined(symbol))
-		error("'%s' already defined at %s(%" PRIu32 ")\n", symbolName,
-			symbol->fileName, symbol->fileLine);
+	else if (sym_IsDefined(symbol)) {
+		error("'%s' already defined at ", symbolName);
+		dumpFilename(symbol);
+		putc('\n', stderr);
+	}
 
 	return symbol;
 }
@@ -395,15 +419,17 @@
 {
 	struct Symbol *sym = findsymbol(symName, NULL);
 
-	if (sym == NULL)
+	if (sym == NULL) {
 		sym = createsymbol(symName);
-	else if (sym_IsDefined(sym) && sym->type != SYM_SET)
-		error("'%s' already defined as %s at %s(%" PRIu32 ")\n",
-			symName, sym->type == SYM_LABEL ? "label" : "constant",
-			sym->fileName, sym->fileLine);
-	else
-		/* TODO: can the scope be incorrect when talking over refs? */
+	} else if (sym_IsDefined(sym) && sym->type != SYM_SET) {
+		error("'%s' already defined as %s at ",
+		      symName, sym->type == SYM_LABEL ? "label" : "constant");
+		dumpFilename(sym);
+		putc('\n', stderr);
+	} else {
+		/* TODO: can the scope be incorrect when taking over refs? */
 		updateSymbolFilename(sym);
+	}
 
 	sym->type = SYM_SET;
 	sym->value = value;
@@ -424,9 +450,12 @@
 	if (!sym) {
 		sym = createsymbol(name);
 	} else if (sym_IsDefined(sym)) {
-		error("'%s' already defined in %s(%" PRIu32 ")\n",
-		      name, sym->fileName, sym->fileLine);
+		error("'%s' already defined at ", name);
+		dumpFilename(sym);
+		putc('\n', stderr);
 		return NULL;
+	} else {
+		updateSymbolFilename(sym);
 	}
 	/* If the symbol already exists as a ref, just "take over" it */
 	sym->type = SYM_LABEL;
@@ -434,7 +463,6 @@
 	if (exportall)
 		sym->isExported = true;
 	sym->section = sect_GetSymbolSection();
-	updateSymbolFilename(sym);
 
 	if (sym && !sym->section)
 		error("Label \"%s\" created outside of a SECTION\n", name);
@@ -517,7 +545,7 @@
 	sym->type = SYM_MACRO;
 	sym->macroSize = size;
 	sym->macro = body;
-	updateSymbolFilename(sym);
+	setSymbolFilename(sym); /* TODO: is this really necessary? */
 	/*
 	 * The symbol is created at the line after the `endm`,
 	 * override this with the actual definition line
@@ -577,10 +605,11 @@
 
 	sym->isBuiltin = true;
 	sym->hasCallback = true;
-	strcpy(sym->fileName, "<builtin>");
+	sym->src = NULL;
 	sym->fileLine = 0;
 	return sym;
 }
+
 /*
  * Initialize the symboltable
  */
--- a/src/asm/warning.c
+++ b/src/asm/warning.c
@@ -202,7 +202,7 @@
 	       char const *flagfmt, char const *flag)
 {
 	fputs(type, stderr);
-	fstk_Dump();
+	fstk_DumpCurrent();
 	fprintf(stderr, flagfmt, flag);
 	vfprintf(stderr, fmt, args);
 	lexer_DumpStringExpansions();
--- a/src/link/assign.c
+++ b/src/link/assign.c
@@ -81,14 +81,14 @@
 
 		/* Check if this doesn't conflict with what the code says */
 		if (section->isBankFixed && placement->bank != section->bank)
-			error("Linker script contradicts \"%s\"'s bank placement",
+			error(NULL, 0, "Linker script contradicts \"%s\"'s bank placement",
 			      section->name);
 		if (section->isAddressFixed && placement->org != section->org)
-			error("Linker script contradicts \"%s\"'s address placement",
+			error(NULL, 0, "Linker script contradicts \"%s\"'s address placement",
 			      section->name);
 		if (section->isAlignFixed
 		 && (placement->org & section->alignMask) != 0)
-			error("Linker script contradicts \"%s\"'s alignment",
+			error(NULL, 0, "Linker script contradicts \"%s\"'s alignment",
 			      section->name);
 
 		section->isAddressFixed = true;
--- a/src/link/main.c
+++ b/src/link/main.c
@@ -6,8 +6,10 @@
  * SPDX-License-Identifier: MIT
  */
 
+#include <assert.h>
 #include <inttypes.h>
 #include <stdbool.h>
+#include <stdarg.h>
 #include <stdio.h>
 #include <stdint.h>
 #include <stdlib.h>
@@ -39,25 +41,73 @@
 
 static uint32_t nbErrors = 0;
 
-void error(char const *fmt, ...)
+/***** Helper function to dump a file stack to stderr *****/
+
+char const *dumpFileStack(struct FileStackNode const *node)
 {
+	char const *lastName;
+
+	if (node->parent) {
+		lastName = dumpFileStack(node->parent);
+		/* REPT nodes use their parent's name */
+		if (node->type != NODE_REPT)
+			lastName = node->name;
+		fprintf(stderr, "(%" PRIu32 ") -> %s", node->lineNo, lastName);
+		if (node->type == NODE_REPT) {
+			for (uint32_t i = 0; i < node->reptDepth; i++)
+				fprintf(stderr, "::REPT~%" PRIu32, node->iters[i]);
+		}
+	} else {
+		assert(node->type != NODE_REPT);
+		lastName = node->name;
+		fputs(lastName, stderr);
+	}
+
+	return lastName;
+}
+
+void warning(struct FileStackNode const *where, uint32_t lineNo, char const *fmt, ...)
+{
 	va_list ap;
 
-	fprintf(stderr, "error: ");
+	fputs("warning: ", stderr);
+	if (where) {
+		dumpFileStack(where);
+		fprintf(stderr, "(%" PRIu32 "): ", lineNo);
+	}
 	va_start(ap, fmt);
 	vfprintf(stderr, fmt, ap);
 	va_end(ap);
 	putc('\n', stderr);
+}
 
+void error(struct FileStackNode const *where, uint32_t lineNo, char const *fmt, ...)
+{
+	va_list ap;
+
+	fputs("error: ", stderr);
+	if (where) {
+		dumpFileStack(where);
+		fprintf(stderr, "(%" PRIu32 "): ", lineNo);
+	}
+	va_start(ap, fmt);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+	putc('\n', stderr);
+
 	if (nbErrors != UINT32_MAX)
 		nbErrors++;
 }
 
-noreturn_ void fatal(char const *fmt, ...)
+noreturn_ void fatal(struct FileStackNode const *where, uint32_t lineNo, char const *fmt, ...)
 {
 	va_list ap;
 
-	fprintf(stderr, "fatal: ");
+	fputs("fatal: ", stderr);
+	if (where) {
+		dumpFileStack(where);
+		fprintf(stderr, "(%" PRIu32 "): ", lineNo);
+	}
 	va_start(ap, fmt);
 	vfprintf(stderr, fmt, ap);
 	va_end(ap);
@@ -177,11 +227,11 @@
 		case 'p':
 			value = strtoul(optarg, &endptr, 0);
 			if (optarg[0] == '\0' || *endptr != '\0') {
-				error("Invalid argument for option 'p'");
+				error(NULL, 0, "Invalid argument for option 'p'");
 				value = 0xFF;
 			}
 			if (value > 0xFF) {
-				error("Argument for 'p' must be a byte (between 0 and 0xFF)");
+				error(NULL, 0, "Argument for 'p' must be a byte (between 0 and 0xFF)");
 				value = 0xFF;
 			}
 			padValue = value;
@@ -189,7 +239,7 @@
 		case 's':
 			/* FIXME: nobody knows what this does, figure it out */
 			(void)optarg;
-			warnx("Nobody has any idea what `-s` does");
+			warning(NULL, 0, "Nobody has any idea what `-s` does");
 			break;
 		case 't':
 			is32kMode = true;
@@ -234,8 +284,8 @@
 		bankranges[SECTTYPE_VRAM][1] = BANK_MIN_VRAM;
 
 	/* Read all object files first, */
-	while (curArgIndex < argc)
-		obj_ReadFile(argv[curArgIndex++]);
+	for (obj_Setup(argc - curArgIndex); curArgIndex < argc; curArgIndex++)
+		obj_ReadFile(argv[curArgIndex], argc - curArgIndex - 1);
 
 	/* then process them, */
 	obj_DoSanityChecks();
--- a/src/link/object.c
+++ b/src/link/object.c
@@ -31,6 +31,11 @@
 	struct SymbolList *next;
 } *symbolLists;
 
+unsigned int nbObjFiles;
+static struct {
+	struct FileStackNode *nodes;
+	uint32_t nbNodes;
+} *nodes;
 static struct Assertion *assertions;
 
 /***** Helper functions for reading object files *****/
@@ -170,12 +175,56 @@
 /***** Functions to parse object files *****/
 
 /**
- * Reads a RGB6 symbol from a file.
+ * Reads a file stack node form a file.
  * @param file The file to read from
+ * @param nodes The file's array of nodes
+ * @param i The ID of the node in the array
+ * @param fileName The filename to report in errors
+ */
+static void readFileStackNode(FILE *file, struct FileStackNode fileNodes[], uint32_t i,
+			      char const *fileName)
+{
+	uint32_t parentID;
+
+	tryReadlong(parentID, file,
+		    "%s: Cannot read node #%" PRIu32 "'s parent ID: %s", fileName, i);
+	fileNodes[i].parent = parentID == -1 ? NULL : &fileNodes[parentID];
+	tryReadlong(fileNodes[i].lineNo, file,
+		    "%s: Cannot read node #%" PRIu32 "'s line number: %s", fileName, i);
+	tryGetc(fileNodes[i].type, file, "%s: Cannot read node #%" PRIu32 "'s type: %s",
+		fileName, i);
+	switch (fileNodes[i].type) {
+	case NODE_FILE:
+	case NODE_MACRO:
+		tryReadstr(fileNodes[i].name, file,
+			   "%s: Cannot read node #%" PRIu32 "'s file name: %s", fileName, i);
+		break;
+
+	case NODE_REPT:
+		tryReadlong(fileNodes[i].reptDepth, file,
+			    "%s: Cannot read node #%" PRIu32 "'s rept depth: %s", fileName, i);
+		fileNodes[i].iters = malloc(sizeof(*fileNodes[i].iters) * fileNodes[i].reptDepth);
+		if (!fileNodes[i].iters)
+			fatal(NULL, 0, "%s: Failed to alloc node #%" PRIu32 "'s iters: %s",
+			      fileName, i, strerror(errno));
+		for (uint32_t k = 0; k < fileNodes[i].reptDepth; k++)
+			tryReadlong(fileNodes[i].iters[k], file,
+				    "%s: Cannot read node #%" PRIu32 "'s iter #%" PRIu32 ": %s",
+				    fileName, i, k);
+		if (!fileNodes[i].parent)
+			fatal(NULL, 0, "%s is not a valid object file: root node (#%"
+			      PRIu32 ") may not be REPT", fileName, i);
+	}
+}
+
+/**
+ * Reads a symbol from a file.
+ * @param file The file to read from
  * @param symbol The struct to fill
  * @param fileName The filename to report in errors
  */
-static void readSymbol(FILE *file, struct Symbol *symbol, char const *fileName)
+static void readSymbol(FILE *file, struct Symbol *symbol,
+		       char const *fileName, struct FileStackNode fileNodes[])
 {
 	tryReadstr(symbol->name, file, "%s: Cannot read symbol name: %s",
 		   fileName);
@@ -184,9 +233,12 @@
 	/* If the symbol is defined in this file, read its definition */
 	if (symbol->type != SYMTYPE_IMPORT) {
 		symbol->objFileName = fileName;
-		tryReadstr(symbol->fileName, file,
-			   "%s: Cannot read \"%s\"'s file name: %s",
+		uint32_t nodeID;
+
+		tryReadlong(nodeID, file,
+			   "%s: Cannot read \"%s\"'s node ID: %s",
 			   fileName, symbol->name);
+		symbol->src = &fileNodes[nodeID];
 		tryReadlong(symbol->lineNo, file,
 			    "%s: Cannot read \"%s\"'s line number: %s",
 			    fileName, symbol->name);
@@ -202,7 +254,7 @@
 }
 
 /**
- * Reads a RGB6 patch from a file.
+ * Reads a patch from a file.
  * @param file The file to read from
  * @param patch The struct to fill
  * @param fileName The filename to report in errors
@@ -210,11 +262,17 @@
  */
 static void readPatch(FILE *file, struct Patch *patch, char const *fileName,
 		      char const *sectName, uint32_t i,
-		      struct Section *fileSections[])
+		      struct Section *fileSections[], struct FileStackNode fileNodes[])
 {
-	tryReadstr(patch->fileName, file,
-		   "%s: Unable to read \"%s\"'s patch #%" PRIu32 "'s name: %s",
+	uint32_t nodeID;
+
+	tryReadlong(nodeID, file,
+		   "%s: Unable to read \"%s\"'s patch #%" PRIu32 "'s node ID: %s",
 		   fileName, sectName, i);
+	patch->src = &fileNodes[nodeID];
+	tryReadlong(patch->lineNo, file,
+		    "%s: Unable to read \"%s\"'s patch #%" PRIu32 "'s line number: %s",
+		    fileName, sectName, i);
 	tryReadlong(patch->offset, file,
 		    "%s: Unable to read \"%s\"'s patch #%" PRIu32 "'s offset: %s",
 		    fileName, sectName, i);
@@ -221,9 +279,8 @@
 	tryReadlong(patch->pcSectionID, file,
 		    "%s: Unable to read \"%s\"'s patch #%" PRIu32 "'s PC offset: %s",
 		    fileName, sectName, i);
-	patch->pcSection = patch->pcSectionID == -1
-					? NULL
-					: fileSections[patch->pcSectionID];
+	patch->pcSection = patch->pcSectionID == -1 ? NULL
+						    : fileSections[patch->pcSectionID];
 	tryReadlong(patch->pcOffset, file,
 		    "%s: Unable to read \"%s\"'s patch #%" PRIu32 "'s PC offset: %s",
 		    fileName, sectName, i);
@@ -234,9 +291,11 @@
 		    "%s: Unable to read \"%s\"'s patch #%" PRIu32 "'s RPN size: %s",
 		    fileName, sectName, i);
 
-	uint8_t *rpnExpression =
-		malloc(sizeof(*rpnExpression) * patch->rpnSize);
-	size_t nbElementsRead = fread(rpnExpression, sizeof(*rpnExpression),
+	patch->rpnExpression = malloc(sizeof(*patch->rpnExpression) * patch->rpnSize);
+	if (!patch->rpnExpression)
+		err(1, "%s: Failed to alloc \"%s\"'s patch #%" PRIu32 "'s RPN expression",
+		    fileName, sectName, i);
+	size_t nbElementsRead = fread(patch->rpnExpression, sizeof(*patch->rpnExpression),
 				      patch->rpnSize, file);
 
 	if (nbElementsRead != patch->rpnSize)
@@ -243,7 +302,6 @@
 		errx(1, "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s RPN expression: %s",
 		     fileName, sectName, i,
 		     feof(file) ? "Unexpected end of file" : strerror(errno));
-	patch->rpnExpression = rpnExpression;
 }
 
 /**
@@ -252,8 +310,8 @@
  * @param section The struct to fill
  * @param fileName The filename to report in errors
  */
-static void readSection(FILE *file, struct Section *section,
-			char const *fileName, struct Section *fileSections[])
+static void readSection(FILE *file, struct Section *section, char const *fileName,
+			struct Section *fileSections[], struct FileStackNode fileNodes[])
 {
 	int32_t tmp;
 	uint8_t byte;
@@ -280,7 +338,7 @@
 		    fileName, section->name);
 	section->isAddressFixed = tmp >= 0;
 	if (tmp > UINT16_MAX) {
-		error("\"%s\"'s org is too large (%" PRId32 ")",
+		error(NULL, 0, "\"%s\"'s org is too large (%" PRId32 ")",
 		      section->name, tmp);
 		tmp = UINT16_MAX;
 	}
@@ -296,7 +354,7 @@
 	tryReadlong(tmp, file, "%s: Cannot read \"%s\"'s alignment offset: %s",
 		    fileName, section->name);
 	if (tmp > UINT16_MAX) {
-		error("\"%s\"'s alignment offset is too large (%" PRId32 ")",
+		error(NULL, 0, "\"%s\"'s alignment offset is too large (%" PRId32 ")",
 		      section->name, tmp);
 		tmp = UINT16_MAX;
 	}
@@ -332,7 +390,7 @@
 			    section->name);
 		for (uint32_t i = 0; i < section->nbPatches; i++) {
 			readPatch(file, &patches[i], fileName, section->name,
-				  i, fileSections);
+				  i, fileSections, fileNodes);
 		}
 		section->patches = patches;
 	}
@@ -375,13 +433,13 @@
  */
 static void readAssertion(FILE *file, struct Assertion *assert,
 			  char const *fileName, uint32_t i,
-			  struct Section *fileSections[])
+			  struct Section *fileSections[], struct FileStackNode fileNodes[])
 {
 	char assertName[sizeof("Assertion #" EXPAND_AND_STR(UINT32_MAX))];
 
 	snprintf(assertName, sizeof(assertName), "Assertion #%" PRIu32, i);
 
-	readPatch(file, &assert->patch, fileName, assertName, 0, fileSections);
+	readPatch(file, &assert->patch, fileName, assertName, 0, fileSections, fileNodes);
 	tryReadstr(assert->message, file, "%s: Cannot read assertion's message: %s",
 		   fileName);
 }
@@ -394,11 +452,7 @@
 	return section;
 }
 
-/**
- * Reads an object file of any supported format
- * @param fileName The filename to report for errors
- */
-void obj_ReadFile(char const *fileName)
+void obj_ReadFile(char const *fileName, unsigned int fileID)
 {
 	FILE *file = strcmp("-", fileName) ? fopen(fileName, "rb") : stdin;
 
@@ -438,6 +492,14 @@
 
 	nbSectionsToAssign += nbSections;
 
+	tryReadlong(nodes[fileID].nbNodes, file, "%s: Cannot read number of nodes: %s", fileName);
+	nodes[fileID].nodes = calloc(nodes[fileID].nbNodes, sizeof(nodes[fileID].nodes[0]));
+	if (!nodes[fileID].nodes)
+		err(1, "Failed to get memory for %s's nodes", fileName);
+	verbosePrint("Reading %u nodes...\n", nodes[fileID].nbNodes);
+	for (uint32_t i = 0; i < nodes[fileID].nbNodes; i++)
+		readFileStackNode(file, nodes[fileID].nodes, i, fileName);
+
 	/* This file's symbols, kept to link sections to them */
 	struct Symbol **fileSymbols =
 		malloc(sizeof(*fileSymbols) * nbSymbols + 1);
@@ -464,7 +526,7 @@
 
 		if (!symbol)
 			err(1, "%s: Couldn't create new symbol", fileName);
-		readSymbol(file, symbol, fileName);
+		readSymbol(file, symbol, fileName, nodes[fileID].nodes);
 
 		fileSymbols[i] = symbol;
 		if (symbol->type == SYMTYPE_EXPORT)
@@ -485,7 +547,7 @@
 			err(1, "%s: Couldn't create new section", fileName);
 
 		fileSections[i]->nextu = NULL;
-		readSection(file, fileSections[i], fileName, fileSections);
+		readSection(file, fileSections[i], fileName, fileSections, nodes[fileID].nodes);
 		fileSections[i]->fileSymbols = fileSymbols;
 		if (nbSymPerSect[i]) {
 			fileSections[i]->symbols = malloc(nbSymPerSect[i]
@@ -535,7 +597,7 @@
 
 		if (!assertion)
 			err(1, "%s: Couldn't create new assertion", fileName);
-		readAssertion(file, assertion, fileName, i, fileSections);
+		readAssertion(file, assertion, fileName, i, fileSections, nodes[fileID].nodes);
 		assertion->fileSymbols = fileSymbols;
 		assertion->next = assertions;
 		assertions = assertion;
@@ -555,6 +617,15 @@
 	patch_CheckAssertions(assertions);
 }
 
+void obj_Setup(unsigned int nbFiles)
+{
+	nbObjFiles = nbFiles;
+
+	if (nbFiles > SIZE_MAX / sizeof(*nodes))
+		fatal(NULL, 0, "Impossible to link more than %zu files!", SIZE_MAX / sizeof(*nodes));
+	nodes = malloc(sizeof(*nodes) * nbFiles);
+}
+
 static void freeSection(struct Section *section, void *arg)
 {
 	(void)arg;
@@ -562,12 +633,8 @@
 	free(section->name);
 	if (sect_HasData(section->type)) {
 		free(section->data);
-		for (int32_t i = 0; i < section->nbPatches; i++) {
-			struct Patch *patch = &section->patches[i];
-
-			free(patch->fileName);
-			free(patch->rpnExpression);
-		}
+		for (int32_t i = 0; i < section->nbPatches; i++)
+			free(section->patches[i].rpnExpression);
 		free(section->patches);
 	}
 	free(section->symbols);
@@ -577,13 +644,20 @@
 static void freeSymbol(struct Symbol *symbol)
 {
 	free(symbol->name);
-	if (symbol->type != SYMTYPE_IMPORT)
-		free(symbol->fileName);
 	free(symbol);
 }
 
 void obj_Cleanup(void)
 {
+	for (unsigned int i = 0; i < nbObjFiles; i++) {
+		for (uint32_t j = 0; j < nodes[i].nbNodes; j++) {
+			if (nodes[i].nodes[j].type == NODE_REPT)
+				free(nodes[i].nodes[j].iters);
+		}
+		free(nodes[i].nodes);
+	}
+	free(nodes);
+
 	sym_CleanupSymbols();
 
 	sect_ForEach(freeSection, NULL);
--- a/src/link/patch.c
+++ b/src/link/patch.c
@@ -6,11 +6,13 @@
  * SPDX-License-Identifier: MIT
  */
 
+#include <assert.h>
 #include <inttypes.h>
 #include <limits.h>
 #include <stdlib.h>
 #include <string.h>
 
+#include "link/object.h"
 #include "link/patch.h"
 #include "link/section.h"
 #include "link/symbol.h"
@@ -104,10 +106,10 @@
 	stack.size++;
 }
 
-static int32_t popRPN(char const *fileName)
+static int32_t popRPN(struct FileStackNode const *node, uint32_t lineNo)
 {
 	if (stack.size == 0)
-		errx(1, "%s: Internal error, RPN stack empty", fileName);
+		fatal(node, lineNo, "Internal error, RPN stack empty");
 
 	stack.size--;
 	return stack.buf[stack.size];
@@ -121,10 +123,11 @@
 /* RPN operators */
 
 static uint32_t getRPNByte(uint8_t const **expression, int32_t *size,
-			   char const *fileName)
+			   struct FileStackNode const *node, uint32_t lineNo)
 {
 	if (!(*size)--)
-		errx(1, "%s: RPN expression overread", fileName);
+		fatal(node, lineNo, "Internal error, RPN expression overread");
+
 	return *(*expression)++;
 }
 
@@ -131,6 +134,7 @@
 static struct Symbol const *getSymbol(struct Symbol const * const *symbolList,
 				      uint32_t index)
 {
+	assert(index != -1); /* PC needs to be handled specially, not here */
 	struct Symbol const *symbol = symbolList[index];
 
 	/* If the symbol is defined elsewhere... */
@@ -150,7 +154,7 @@
 			      struct Symbol const * const *fileSymbols)
 {
 /* Small shortcut to avoid a lot of repetition */
-#define popRPN() popRPN(patch->fileName)
+#define popRPN() popRPN(patch->src, patch->lineNo)
 
 	uint8_t const *expression = patch->rpnExpression;
 	int32_t size = patch->rpnSize;
@@ -159,7 +163,7 @@
 
 	while (size > 0) {
 		enum RPNCommand command = getRPNByte(&expression, &size,
-						     patch->fileName);
+						     patch->src, patch->lineNo);
 		int32_t value;
 
 		/*
@@ -187,7 +191,7 @@
 		case RPN_DIV:
 			value = popRPN();
 			if (value == 0) {
-				error("%s: Division by 0", patch->fileName);
+				error(patch->src, patch->lineNo, "Division by 0");
 				popRPN();
 				value = INT32_MAX;
 			} else {
@@ -197,7 +201,7 @@
 		case RPN_MOD:
 			value = popRPN();
 			if (value == 0) {
-				error("%s: Modulo by 0", patch->fileName);
+				error(patch->src, patch->lineNo, "Modulo by 0");
 				popRPN();
 				value = 0;
 			} else {
@@ -269,17 +273,17 @@
 			value = 0;
 			for (uint8_t shift = 0; shift < 32; shift += 8)
 				value |= getRPNByte(&expression, &size,
-						    patch->fileName) << shift;
+						    patch->src, patch->lineNo) << shift;
 			symbol = getSymbol(fileSymbols, value);
 
 			if (!symbol) {
-				error("%s: Requested BANK() of symbol \"%s\", which was not found",
-				      patch->fileName,
+				error(patch->src, patch->lineNo,
+				      "Requested BANK() of symbol \"%s\", which was not found",
 				      fileSymbols[value]->name);
 				value = 1;
 			} else if (!symbol->section) {
-				error("%s: Requested BANK() of non-label symbol \"%s\"",
-				      patch->fileName,
+				error(patch->src, patch->lineNo,
+				      "Requested BANK() of non-label symbol \"%s\"",
 				      fileSymbols[value]->name);
 				value = 1;
 			} else {
@@ -289,14 +293,15 @@
 
 		case RPN_BANK_SECT:
 			name = (char const *)expression;
-			while (getRPNByte(&expression, &size, patch->fileName))
+			while (getRPNByte(&expression, &size, patch->src, patch->lineNo))
 				;
 
 			sect = sect_GetSection(name);
 
 			if (!sect) {
-				error("%s: Requested BANK() of section \"%s\", which was not found",
-				      patch->fileName, name);
+				error(patch->src, patch->lineNo,
+				      "Requested BANK() of section \"%s\", which was not found",
+				      name);
 				value = 1;
 			} else {
 				value = sect->bank;
@@ -305,7 +310,8 @@
 
 		case RPN_BANK_SELF:
 			if (!patch->pcSection) {
-				error("%s: PC has no bank outside a section");
+				error(patch->src, patch->lineNo,
+				      "PC has no bank outside a section");
 				value = 1;
 			} else {
 				value = patch->pcSection->bank;
@@ -317,8 +323,8 @@
 			if (value < 0
 			 || (value > 0xFF && value < 0xFF00)
 			 || value > 0xFFFF)
-				error("%s: Value %" PRId32 " is not in HRAM range",
-				      patch->fileName, value);
+				error(patch->src, patch->lineNo,
+				      "Value %" PRId32 " is not in HRAM range", value);
 			value &= 0xFF;
 			break;
 
@@ -328,8 +334,8 @@
 			 * They can be easily checked with a bitmask
 			 */
 			if (value & ~0x38)
-				error("%s: Value %" PRId32 " is not a RST vector",
-				      patch->fileName, value);
+				error(patch->src, patch->lineNo,
+				      "Value %" PRId32 " is not a RST vector", value);
 			value |= 0xC7;
 			break;
 
@@ -337,7 +343,7 @@
 			value = 0;
 			for (uint8_t shift = 0; shift < 32; shift += 8)
 				value |= getRPNByte(&expression, &size,
-						    patch->fileName) << shift;
+						    patch->src, patch->lineNo) << shift;
 			break;
 
 		case RPN_SYM:
@@ -344,25 +350,28 @@
 			value = 0;
 			for (uint8_t shift = 0; shift < 32; shift += 8)
 				value |= getRPNByte(&expression, &size,
-						    patch->fileName) << shift;
+						    patch->src, patch->lineNo) << shift;
 
-			symbol = getSymbol(fileSymbols, value);
-
-			if (!symbol) {
-				error("%s: Unknown symbol \"%s\"",
-				      patch->fileName,
-				      fileSymbols[value]->name);
-			} else if (strcmp(symbol->name, "@")) {
-				value = symbol->value;
-				/* Symbols attached to sections have offsets */
-				if (symbol->section)
-					value += symbol->section->org;
-			} else if (!patch->pcSection) {
-				error("%s: PC has no value outside a section",
-				      patch->fileName);
-				value = 0;
+			if (value == -1) { /* PC */
+				if (!patch->pcSection) {
+					error(patch->src, patch->lineNo,
+					      "PC has no value outside a section");
+					value = 0;
+				} else {
+					value = patch->pcOffset + patch->pcSection->org;
+				}
 			} else {
-				value = patch->pcOffset + patch->pcSection->org;
+				symbol = getSymbol(fileSymbols, value);
+
+				if (!symbol) {
+					error(patch->src, patch->lineNo,
+					      "Unknown symbol \"%s\"", fileSymbols[value]->name);
+				} else {
+					value = symbol->value;
+					/* Symbols attached to sections have offsets */
+					if (symbol->section)
+						value += symbol->section->org;
+				}
 			}
 			break;
 		}
@@ -371,8 +380,8 @@
 	}
 
 	if (stack.size > 1)
-		error("%s: RPN stack has %zu entries on exit, not 1",
-		      patch->fileName, stack.size);
+		error(patch->src, patch->lineNo,
+		      "RPN stack has %zu entries on exit, not 1", stack.size);
 
 	return popRPN();
 
@@ -390,20 +399,20 @@
 							assert->fileSymbols)) {
 			switch ((enum AssertionType)assert->patch.type) {
 			case ASSERT_FATAL:
-				fatal("%s: %s", assert->patch.fileName,
+				fatal(assert->patch.src, assert->patch.lineNo, "%s",
 				      assert->message[0] ? assert->message
 							 : "assert failure");
 				/* Not reached */
 				break; /* Here so checkpatch doesn't complain */
 			case ASSERT_ERROR:
-				error("%s: %s", assert->patch.fileName,
+				error(assert->patch.src, assert->patch.lineNo, "%s",
 				      assert->message[0] ? assert->message
 							 : "assert failure");
 				break;
 			case ASSERT_WARN:
-				warnx("%s: %s", assert->patch.fileName,
-				      assert->message[0] ? assert->message
-							 : "assert failure");
+				warning(assert->patch.src, assert->patch.lineNo, "%s",
+					assert->message[0] ? assert->message
+							   : "assert failure");
 				break;
 			}
 		}
@@ -442,8 +451,9 @@
 			int16_t jumpOffset = value - address;
 
 			if (jumpOffset < -128 || jumpOffset > 127)
-				error("%s: jr target out of reach (expected -129 < %" PRId16 " < 128)",
-				      patch->fileName, jumpOffset);
+				error(patch->src, patch->lineNo,
+				      "jr target out of reach (expected -129 < %" PRId16 " < 128)",
+				      jumpOffset);
 			dataSection->data[offset] = jumpOffset & 0xFF;
 		} else {
 			/* Patch a certain number of bytes */
@@ -459,9 +469,9 @@
 
 			if (value < types[patch->type].min
 			 || value > types[patch->type].max)
-				error("%s: Value %#" PRIx32 "%s is not %u-bit",
-				      patch->fileName, value,
-				      value < 0 ? " (maybe negative?)" : "",
+				error(patch->src, patch->lineNo,
+				      "Value %#" PRIx32 "%s is not %u-bit",
+				      value, value < 0 ? " (maybe negative?)" : "",
 				      types[patch->type].size * 8U);
 			for (uint8_t i = 0; i < types[patch->type].size; i++) {
 				dataSection->data[offset + i] = value & 0xFF;
--- a/src/link/symbol.c
+++ b/src/link/symbol.c
@@ -8,9 +8,12 @@
 
 #include <inttypes.h>
 #include <stdbool.h>
+#include <stdlib.h>
 
+#include "link/object.h"
 #include "link/symbol.h"
 #include "link/main.h"
+
 #include "extern/err.h"
 #include "hashmap.h"
 
@@ -40,11 +43,15 @@
 	/* Check if the symbol already exists */
 	struct Symbol *other = hash_GetElement(symbols, symbol->name);
 
-	if (other)
-		errx(1, "\"%s\" both in %s from %s(%" PRId32 ") and in %s from %s(%" PRId32 ")",
-		     symbol->name,
-		     symbol->objFileName, symbol->fileName, symbol->lineNo,
-		      other->objFileName,  other->fileName,  other->lineNo);
+	if (other) {
+		fprintf(stderr, "error: \"%s\" both in %s from ", symbol->name, symbol->objFileName);
+		dumpFileStack(symbol->src);
+		fprintf(stderr, "(%" PRIu32 ") and in %s from ",
+			symbol->lineNo, other->objFileName);
+		dumpFileStack(other->src);
+		fprintf(stderr, "(%" PRIu32 ")\n", other->lineNo);
+		exit(1);
+	}
 
 	/* If not, add it */
 	bool collided = hash_AddElement(symbols, symbol->name, symbol);
--- a/src/rgbds.5
+++ b/src/rgbds.5
@@ -16,7 +16,7 @@
 .Xr rgbasm 1
 and
 .Xr rgblink 1 .
-.Em Please note that the specifications may change.
+.Em Please note that the specifications may change .
 This toolchain is in development and new features may require adding more information to the current format, or modifying some fields, which would break compatibility with older versions.
 .Pp
 .Sh FILE STRUCTURE
@@ -34,34 +34,67 @@
 ; Header
 
 BYTE    ID[4]            ; "RGB9"
-LONG    RevisionNumber   ; The format's revision number this file uses
-LONG    NumberOfSymbols  ; The number of symbols used in this file
-LONG    NumberOfSections ; The number of sections used in this file
+LONG    RevisionNumber   ; The format's revision number this file uses.
+LONG    NumberOfSymbols  ; The number of symbols used in this file.
+LONG    NumberOfSections ; The number of sections used in this file.
 
+; File info
+
+LONG    NumberOfNodes       ; The number of nodes contained in this file.
+
+REPT NumberOfNodes          ; IMPORTANT NOTE: the nodes are actually written in
+                            ; **reverse** order, meaningthe node with ID 0 is
+                            ; the last one in the file!
+
+    LONG    ParentID        ; ID of the parent node, -1 means this is the root.
+
+    LONG    ParentLineNo    ; Line at which the parent context was exited.
+                            ; Meaningless on the root node.
+
+    BYTE    Type            ; 0 = REPT node
+                            ; 1 = File node
+                            ; 2 = Macro node
+
+    IF Type != 0            ; If the node is not a REPT...
+
+        STRING  Name        ; The node's name: either a file name, or macro name
+                            ; prefixed by its definition file name.
+
+    ELSE                    ; If the node is a REPT, it also contains the iter
+                            ; counts of all the parent REPTs.
+
+        LONG    Depth       ; Size of the array below.
+
+        LONG    Iter[Depth] ; The number of REPT iterations by increasing depth.
+
+    ENDC
+
+ENDR
+
 ; Symbols
 
-REPT    NumberOfSymbols   ; Number of symbols defined in this object file.
+REPT    NumberOfSymbols    ; Number of symbols defined in this object file.
 
-    STRING  Name          ; The name of this symbol. Local symbols are stored
-                          ; as "Scope.Symbol".
+    STRING  Name           ; The name of this symbol. Local symbols are stored
+                           ; as "Scope.Symbol".
 
-    BYTE    Type          ; 0 = LOCAL symbol only used in this file.
-                          ; 1 = IMPORT this symbol from elsewhere
-                          ; 2 = EXPORT this symbol to other objects.
+    BYTE    Type           ; 0 = LOCAL symbol only used in this file.
+                           ; 1 = IMPORT this symbol from elsewhere
+                           ; 2 = EXPORT this symbol to other objects.
 
-    IF (Type & 0x7F) != 1 ; If symbol is defined in this object file.
+    IF (Type & 0x7F) != 1  ; If symbol is defined in this object file.
 
-        STRING  FileName  ; File where the symbol is defined.
+        LONG    SourceFile ; File where the symbol is defined.
 
-        LONG    LineNum   ; Line number in the file where the symbol is defined.
+        LONG    LineNum    ; Line number in the file where the symbol is defined.
 
-        LONG    SectionID ; The section number (of this object file) in which
-                          ; this symbol is defined. If it doesn't belong to any
-                          ; specific section (like a constant), this field has
-                          ; the value -1.
+        LONG    SectionID  ; The section number (of this object file) in which
+                           ; this symbol is defined. If it doesn't belong to any
+                           ; specific section (like a constant), this field has
+                           ; the value -1.
 
-        LONG    Value     ; The symbols value. It's the offset into that
-                          ; symbol's section.
+        LONG    Value      ; The symbols value. It's the offset into that
+                           ; symbol's section.
 
     ENDC
 
@@ -107,9 +140,11 @@
 
         REPT    NumberOfPatches
 
-            STRING  SourceFile   ; Name of the source file (for printing error
-                                 ; messages).
+            LONG    SourceFile   ; ID of the source file node (for printing
+                                 ; error messages).
 
+            LONG    LineNo       ; Line at which the patch was created.
+
             LONG    Offset       ; Offset into the section where patch should
                                  ; be applied (in bytes).
 
@@ -145,8 +180,10 @@
 
 REPT  NumberOfAssertions
 
-  STRING  SourceFile   ; Name of the source file (for printing the failure).
+  LONG    SourceFile   ; ID of the source file node (for printing the failure).
 
+  LONG    LineNo       ; Line at which the assertion was created.
+
   LONG    Offset       ; Offset into the section where the assertion is located.
 
   LONG    SectionID    ; Index within the file of the section in which PC is
@@ -209,7 +246,7 @@
 .It Li $50 Ta Li BANK(symbol) ,
 a
 .Ar LONG
-Symbol ID follows.
+Symbol ID follows, where -1 means PC
 .It Li $51 Ta Li BANK(section_name) ,
 a null-terminated string follows.
 .It Li $52 Ta Li Current BANK()
--- a/test/asm/label-redefinition.err
+++ b/test/asm/label-redefinition.err
@@ -1,3 +1,3 @@
 ERROR: label-redefinition.asm(7):
-    'Sym' already defined in label-redefinition.asm::m(4)
+    'Sym' already defined at label-redefinition.asm(6) -> label-redefinition.asm::m(4)
 error: Assembly aborted (1 errors)!