Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion include/asm/warning.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,17 @@ extern Diagnostics<WarningLevel, WarningID> warnings;
void warning(WarningID id, char const *fmt, ...);

// Used for errors that compromise the whole assembly process by affecting the
// following code, potencially making the assembler generate errors caused by
// following code, potentially making the assembler generate errors caused by
// the first one and unrelated to the code that the assembler complains about.
// It is also used when the assembler goes into an invalid state (for example,
// when it fails to allocate memory).
[[gnu::format(printf, 1, 2), noreturn]]
void fatal(char const *fmt, ...);

// Used for fatal errors that handle their own backtrace output.
[[noreturn]]
void fatalNoTrace(std::function<void()> callback);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use std::function unless you have to. Its indirection has a lot of overhead, including in codegen, due to the type-erasure that it performs.

If you want a callable that can hold onto state (thus, not just a function pointer), please template this function. (Using concepts to document the expected parameters and return type, of course.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a fatal error; the overhead of a memory allocation compared to passing a function pointer really won't matter. Regardless, I'd be fine with changing the implementation, if we can do so without moving the whole implementation into the .hpp file as a template (and thereby also having to include the appropriate headers, helper functions, etc).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦀

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remain concerned about how much extra C++ standard library this pulls in, the extra headers involved, and all the extra codegen regardless of the runtime cost.

Copy link
Copy Markdown
Contributor Author

@Rangi42 Rangi42 May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if it can be done another way, but I suspect I chose std::function in the first place for errorNoTrace because the callbacks need to capture variables, and the function can't/shouldn't be defined as a template in the asm/warning.hpp header file. If the #include <functional> has enough extra codegen to matter, it should be measurable, not just "avoid std::function all the time".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦀

How would this be solved in Rust? Can format! args be bundled into a Vec of deferred arg packs?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, using format_args!(); but rather, you don't need to provide declaration separately from implementation and thus there are no header woes anymore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we move fatalNoTrace into the warning.hpp header file, we should do the same for the already-existing errorNoTrace function, which also takes a std::function<void()> callback. However, errorNoTrace calls the static void incrementErrors() function, so then that whole function would have to be non-static to be visible in the header file. Also the header would have to have #include <stdio.h> and #include "style.hpp" for the functions' implementations. I just doing think avoiding std::function is worth that cost.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::function is likely to have a "small buffer optimization", and since sectAlreadyDefinedCallback only captures one reference-pointer, it's likely to fit in a small buffer and not require an extra heap allocation. Unlike a template parameter, it won't be inlined, but that's just the time+space cost of one vtable'd function call. It's not done in some tight loop, it's only done in error conditions, and fatalNoTrace exits immediately so it will only happen at most once. So the runtime+space cost of std::function is low, and the code quality benefit is worthwhile.


// Used for errors that make it impossible to assemble correctly, but don't
// affect the following code. The code will fail to assemble but the user will
// get a list of all errors at the end, making it easier to fix all of them at
Expand Down
130 changes: 89 additions & 41 deletions src/asm/section.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <iterator>
#include <optional>
#include <stack>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -114,16 +115,29 @@ Section *sect_FindSectionByName(std::string const &name) {
return index ? &sections[*index] : nullptr;
}

#define sectError(...) \
do { \
error(__VA_ARGS__); \
++nbSectErrors; \
} while (0)
static std::vector<std::string> sectErrors;

static unsigned int
mergeSectUnion(Section &sect, uint32_t org, uint8_t alignment, uint16_t alignOffset) {
unsigned int nbSectErrors = 0;
void sectError(char const *fmt, ...) {
std::string result;
va_list args1, args2;
va_start(args1, fmt);
va_copy(args2, args1);
int len = vsnprintf(nullptr, 0, fmt, args1);
va_end(args1);
if (len < 0) {
// LCOV_EXCL_START
va_end(args2);
fatal("Error describing the error that occurred when merging a section");
// LCOV_EXCL_STOP
} else if (len > 0) {
result.resize(len);
vsnprintf(result.data(), len + 1, fmt, args2);
}
va_end(args2);
sectErrors.push_back(result);
}
Comment on lines +120 to +138
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really hate the logic of this function, and would rather use std::format.

Copy link
Copy Markdown
Contributor Author

@Rangi42 Rangi42 May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rely on std::format being available on all platforms/compilers? (Like how std::filesystem isn't quite usable either.) If so, I'd like to replace all our message-formatting code with it, instead of fprintfs.

Even without std::function, I could make this be a template function instead of a stdarg.h one, if you prefer?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please. As for availability, I have no idea.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use a template function instead:

template<typename... Args>
void sectError(char const *fmt, Args... args) {
	std::string result;
	int len = snprintf(nullptr, 0, fmt, std::forward<Args>(args)...);
	if (len < 0) {
		fatal("Error describing the error that occurred when merging a section"); // LCOV_EXCL_LINE
	} else if (len > 0) {
		result.resize(len);
		snprintf(result.data(), len + 1, fmt, std::forward<Args>(args)...);
	}
	sectErrors.push_back(result);
}

...however, snprintf then complains "warning: format not a string literal and no format arguments", and I'd rather not disable -Wno-format-security just for this.

Switching to std::format can be done for #1973.


static void mergeSectUnion(Section &sect, uint32_t org, uint8_t alignment, uint16_t alignOffset) {
assume(alignment < 16); // Should be ensured by the caller
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
Expand All @@ -136,11 +150,15 @@ static unsigned int
// If both are fixed, they must be the same
if (sect.org != UINT32_MAX && sect.org != org) {
sectError(
"Section already declared as fixed at different address $%04" PRIx32, sect.org
"Section \"%s\" already declared as fixed at different address $%04" PRIx32,
sect.name.c_str(),
sect.org
);
} else if (sect.align != 0 && ((org - sect.alignOfs) & sectAlignMask)) {
sectError(
"Section already declared as aligned to %" PRIu32 " bytes (offset %" PRIu16 ")",
"Section \"%s\" already declared as aligned to %" PRIu32 " bytes (offset %" PRIu16
")",
sect.name.c_str(),
sectAlignSize,
sect.alignOfs
);
Expand All @@ -154,15 +172,17 @@ static unsigned int
if (sect.org != UINT32_MAX) {
if ((sect.org - alignOffset) & alignMask) {
sectError(
"Section already declared as fixed at incompatible address $%04" PRIx32,
"Section \"%s\" already declared as fixed at incompatible address $%04" PRIx32,
sect.name.c_str(),
sect.org
);
}
// Check if alignment offsets are compatible
} else if ((alignOffset & sectAlignMask) != (sect.alignOfs & alignMask)) {
sectError(
"Section already declared with incompatible %" PRIu32
"Section \"%s\" already declared with incompatible %" PRIu32
"-byte alignment (offset %" PRIu16 ")",
sect.name.c_str(),
sectAlignSize,
sect.alignOfs
);
Expand All @@ -172,14 +192,9 @@ static unsigned int
sect.alignOfs = alignOffset;
}
}

return nbSectErrors;
}

static unsigned int
mergeFragments(Section &sect, uint32_t org, uint8_t alignment, uint16_t alignOffset) {
unsigned int nbSectErrors = 0;

static void mergeFragments(Section &sect, uint32_t org, uint8_t alignment, uint16_t alignOffset) {
assume(alignment < 16); // Should be ensured by the caller
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
Expand All @@ -197,11 +212,15 @@ static unsigned int
// If both are fixed, they must be the same
if (sect.org != UINT32_MAX && sect.org != curOrg) {
sectError(
"Section already declared as fixed at incompatible address $%04" PRIx32, sect.org
"Section \"%s\" already declared as fixed at incompatible address $%04" PRIx32,
sect.name.c_str(),
sect.org
);
} else if (sect.align != 0 && ((curOrg - sect.alignOfs) & sectAlignMask)) {
sectError(
"Section already declared as aligned to %" PRIu32 " bytes (offset %" PRIu16 ")",
"Section \"%s\" already declared as aligned to %" PRIu32 " bytes (offset %" PRIu16
")",
sect.name.c_str(),
sectAlignSize,
sect.alignOfs
);
Expand All @@ -215,15 +234,17 @@ static unsigned int
if (uint32_t curOfs = (alignOffset - sect.size) & alignMask; sect.org != UINT32_MAX) {
if ((sect.org - curOfs) & alignMask) {
sectError(
"Section already declared as fixed at incompatible address $%04" PRIx32,
"Section \"%s\" already declared as fixed at incompatible address $%04" PRIx32,
sect.name.c_str(),
sect.org
);
}
// Check if alignment offsets are compatible
} else if ((curOfs & sectAlignMask) != (sect.alignOfs & alignMask)) {
sectError(
"Section already declared with incompatible %" PRIu32
"Section \"%s\" already declared with incompatible %" PRIu32
"-byte alignment (offset %" PRIu16 ")",
sect.name.c_str(),
sectAlignSize,
sect.alignOfs
);
Expand All @@ -233,8 +254,6 @@ static unsigned int
sect.alignOfs = curOfs;
}
}

return nbSectErrors;
}

static void mergeSections(
Expand All @@ -246,59 +265,88 @@ static void mergeSections(
uint16_t alignOffset,
SectionModifier mod
) {
unsigned int nbSectErrors = 0;
sectErrors.clear();

auto sectAlreadyDefinedCallback = [&sect]() {
fprintf(stderr, "Section \"%s\" already defined\n", sect.name.c_str());
fstk_TraceCurrent();
fputs(" and also:\n", stderr);
sect.src->printBacktrace(sect.fileLine);
};
auto sectAlreadyDefinedError = []() {
// The empty string is a sentinel for the `sectAlreadyDefinedCallback` error,
// since it cannot be preformatted as a string
sectErrors.push_back("");
};

if (type != sect.type) {
sectError(
"Section already exists but with type `%s`", sectionTypeInfo[sect.type].name.c_str()
"Section \"%s\" already exists but with type `%s`",
sect.name.c_str(),
sectionTypeInfo[sect.type].name.c_str()
);
}

if (sect.modifier != mod) {
sectError("Section already declared as `SECTION %s`", sectionModNames[sect.modifier]);
sectError(
"Section \"%s\" already declared as `SECTION %s`",
sect.name.c_str(),
sectionModNames[sect.modifier]
);
} else {
switch (mod) {
case SECTION_UNION:
case SECTION_FRAGMENT: {
unsigned int (*merge)(Section &, uint32_t, uint8_t, uint16_t) =
void (*merge)(Section &, uint32_t, uint8_t, uint16_t) =
mod == SECTION_UNION ? mergeSectUnion : mergeFragments;
nbSectErrors += merge(sect, org, alignment, alignOffset);
merge(sect, org, alignment, alignOffset);

// If the section's bank is unspecified, override it
if (sect.bank == UINT32_MAX) {
sect.bank = bank;
}
// If both specify a bank, it must be the same one
else if (bank != UINT32_MAX && sect.bank != bank) {
sectError("Section already declared with different bank %" PRIu32, sect.bank);
sectError(
"Section \"%s\" already declared with different bank %" PRIu32,
sect.name.c_str(),
sect.bank
);
}
break;
}

case SECTION_NORMAL:
errorNoTrace([&]() {
fputs("Section already defined\n", stderr);
fstk_TraceCurrent();
fputs(" and also:\n", stderr);
sect.src->printBacktrace(sect.fileLine);
++nbSectErrors;
});
sectAlreadyDefinedError();
break;
}
}

if (nbSectErrors) {
if (size_t nbSectErrors = sectErrors.size(); nbSectErrors == 1) {
// If there was only one error, print it as a fatal error
if (std::string const &message = sectErrors[0]; message.empty()) {
fatalNoTrace(sectAlreadyDefinedCallback);
} else {
fatal("%s", message.c_str());
}
} else if (nbSectErrors > 1) {
// If there were multiple errors, print each of them, followed by a fatal summary error
for (std::string const &message : sectErrors) {
if (message.empty()) {
errorNoTrace(sectAlreadyDefinedCallback);
} else {
error("%s", message.c_str());
}
}
fatal(
"Cannot create section \"%s\" (%u error%s)",
"Cannot create section \"%s\" (%zu error%s)",
sect.name.c_str(),
nbSectErrors,
nbSectErrors == 1 ? "" : "s"
);
}
}

#undef sectError

static Section *createSection(
std::string const &name,
SectionType type,
Expand Down
10 changes: 10 additions & 0 deletions src/asm/warning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ void fatal(char const *fmt, ...) {
exit(1);
}

[[noreturn]]
void fatalNoTrace(std::function<void()> callback) {
Comment thread
ISSOtm marked this conversation as resolved.
style_Set(stderr, STYLE_RED, true);
fputs("FATAL: ", stderr);
style_Reset(stderr);
callback();

exit(1);
}

void requireZeroErrors() {
if (warnings.nbErrors != 0) {
style_Set(stderr, STYLE_RED, true);
Expand Down
4 changes: 1 addition & 3 deletions test/asm/duplicate-section-after-literal.err
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
error: Section already defined
FATAL: Section "dup" already defined
at duplicate-section-after-literal.asm(7)
and also:
at duplicate-section-after-literal.asm(5)
FATAL: Cannot create section "dup" (1 error)
at duplicate-section-after-literal.asm(7)
4 changes: 1 addition & 3 deletions test/asm/duplicate-section.err
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
error: Section already defined
FATAL: Section "sec" already defined
at duplicate-section.asm(4)
and also:
at duplicate-section.asm(2)
FATAL: Cannot create section "sec" (1 error)
at duplicate-section.asm(4)
4 changes: 1 addition & 3 deletions test/asm/fragment-align-mismatch.err
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
error: Section already declared as fixed at incompatible address $c002
at fragment-align-mismatch.asm(2)
FATAL: Cannot create section "aligned" (1 error)
FATAL: Section "aligned" already declared as fixed at incompatible address $c002
at fragment-align-mismatch.asm(2)
4 changes: 1 addition & 3 deletions test/asm/fragment-mismatch.err
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
error: Section already declared as fixed at incompatible address $0000
at fragment-mismatch.asm(2)
FATAL: Cannot create section "test" (1 error)
FATAL: Section "test" already declared as fixed at incompatible address $0000
at fragment-mismatch.asm(2)
4 changes: 1 addition & 3 deletions test/asm/incompatible-alignment.err
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
error: Section already declared with incompatible 256-byte alignment (offset 0)
at incompatible-alignment.asm(8)
FATAL: Cannot create section "Test" (1 error)
FATAL: Section "Test" already declared with incompatible 256-byte alignment (offset 0)
at incompatible-alignment.asm(8)
3 changes: 3 additions & 0 deletions test/asm/multiple-section-errors.asm
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SECTION FRAGMENT "test", ROM0[$0000]

SECTION FRAGMENT "test", ROMX[$4000]
6 changes: 6 additions & 0 deletions test/asm/multiple-section-errors.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
error: Section "test" already exists but with type `ROM0`
at multiple-section-errors.asm(3)
error: Section "test" already declared as fixed at incompatible address $0000
at multiple-section-errors.asm(3)
FATAL: Cannot create section "test" (2 errors)
at multiple-section-errors.asm(3)
4 changes: 1 addition & 3 deletions test/asm/section-union-mismatch.err
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
error: Section already declared as fixed at different address $c000
at section-union-mismatch.asm(2)
FATAL: Cannot create section "test" (1 error)
FATAL: Section "test" already declared as fixed at different address $c000
at section-union-mismatch.asm(2)
4 changes: 1 addition & 3 deletions test/asm/section-union.err
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
error: Section already declared as `SECTION UNION`
at section-union.asm(37)
FATAL: Cannot create section "test" (1 error)
FATAL: Section "test" already declared as `SECTION UNION`
at section-union.asm(37)
4 changes: 1 addition & 3 deletions test/asm/union-mismatch.err
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
error: Section already declared as fixed at incompatible address $c001
at union-mismatch.asm(2)
FATAL: Cannot create section "fixed" (1 error)
FATAL: Section "fixed" already declared as fixed at incompatible address $c001
at union-mismatch.asm(2)
4 changes: 1 addition & 3 deletions test/link/section-fragment/align-conflict.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ FATAL: Section "conflicting alignment" is defined with 4-byte alignment (offset
at section-fragment/align-conflict.asm(7)
Linking aborted with 1 error
---
error: Section already declared as aligned to 4 bytes (offset 0)
at <stdin>(18)
FATAL: Cannot create section "conflicting alignment" (1 error)
FATAL: Section "conflicting alignment" already declared as aligned to 4 bytes (offset 0)
at <stdin>(18)
4 changes: 1 addition & 3 deletions test/link/section-union/align-conflict.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ FATAL: Section "conflicting alignment" is defined with 4-byte alignment (offset
at section-union/align-conflict.asm(7)
Linking aborted with 1 error
---
error: Section already declared as aligned to 4 bytes (offset 0)
at <stdin>(18)
FATAL: Cannot create section "conflicting alignment" (1 error)
FATAL: Section "conflicting alignment" already declared as aligned to 4 bytes (offset 0)
at <stdin>(18)
4 changes: 1 addition & 3 deletions test/link/section-union/align-ofs-conflict.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ FATAL: Section "conflicting alignment" is defined with 8-byte alignment (offset
at section-union/align-ofs-conflict.asm(7)
Linking aborted with 1 error
---
error: Section already declared with incompatible 8-byte alignment (offset 7)
at <stdin>(18)
FATAL: Cannot create section "conflicting alignment" (1 error)
FATAL: Section "conflicting alignment" already declared with incompatible 8-byte alignment (offset 7)
at <stdin>(18)
4 changes: 1 addition & 3 deletions test/link/section-union/bad-types.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ FATAL: Section "conflicting types" is defined with type `HRAM`, but also with ty
at section-union/bad-types.asm(7)
Linking aborted with 1 error
---
error: Section already exists but with type `HRAM`
at <stdin>(18)
FATAL: Cannot create section "conflicting types" (1 error)
FATAL: Section "conflicting types" already exists but with type `HRAM`
at <stdin>(18)
4 changes: 1 addition & 3 deletions test/link/section-union/bank-conflict.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ FATAL: Section "conflicting banks" is defined with bank 4, but also with bank 1
at section-union/bank-conflict.asm(5)
Linking aborted with 1 error
---
error: Section already declared with different bank 4
at <stdin>(14)
FATAL: Cannot create section "conflicting banks" (1 error)
FATAL: Section "conflicting banks" already declared with different bank 4
at <stdin>(14)
Loading
Loading