Compare commits

...

154 Commits

Author SHA1 Message Date
Rangi
8bedd710d7 Simplify musl's getopt, including removing optind, optopt, opterr, idx, and longonly 2025-10-23 13:39:28 -04:00
Rangi
efb5a88edb Show conventional colored "error:"/"FATAL:" for CLI option errors 2025-10-23 12:40:29 -04:00
Rangi
f065243cd2 Enable RGBGFX's CLI "at-files" for all programs (#1848) 2025-10-22 17:05:59 -04:00
Rangi
a0bb830679 More consistent man page descriptions for CLI options 2025-10-22 14:48:24 -04:00
Rangi
7654c6e27a More consistent man page descriptions for warning diagnostics 2025-10-22 14:03:34 -04:00
Rangi
400375b2e5 Share some handling between two tests of rgbasm -M - 2025-10-20 20:57:48 -04:00
Rangi
7462bccb72 Move struct Palette into its own file (#1850) 2025-10-20 16:59:24 -04:00
Rangi
2873e0b8c8 Use musttail attribute to guarantee tail recursion (#1849) 2025-10-20 15:56:22 -04:00
Rangi
1badba03d8 Clean up some #define callables
These are used where anonymous functions would not be sufficient
2025-10-13 13:14:49 -04:00
Rangi42
64bcef99bd Lower default -Wtrunction= level to 1 2025-10-13 11:48:33 -04:00
Rangi42
aa672bbec9 Rephrase PURGE documentation and raise the default level to 2
Fixes #1847
2025-10-13 11:43:29 -04:00
Rangi42
651877e094 Avoid reusing a static local variable (too fragile) 2025-10-08 21:19:23 -04:00
Rangi
26c48cc409 Add RGBGFX test for libpng warning with invalid bKGD chunk 2025-10-08 15:44:15 -04:00
Rangi
23b9039716 Give clearer names to template parameters 2025-10-08 14:55:43 -04:00
Rangi
711fba5e35 Add more tests for things that only the external tests had covered 2025-10-08 13:32:48 -04:00
Rangi
089fc11e31 A local label starting with a keyword (e.g. jr.local) is an error 2025-10-08 12:23:08 -04:00
Rangi42
837f552987 Fix bank increment never happening due to unsigned overflow 2025-10-07 16:20:24 -04:00
Rangi
cb8c973453 Add test for undefined __SCOPE__ 2025-10-06 17:51:21 -04:00
Rangi
cca3794dd0 Mention libpng in its internal warning and error messages 2025-10-06 17:03:51 -04:00
Rangi
02c2408f58 Implement reversed template for reversing for-each loops 2025-10-06 16:50:47 -04:00
Rangi
fba0562650 Fix repeated REPT nodes in backtraces 2025-10-06 16:36:55 -04:00
Rangi
0c9920d4a6 Use C++ iterator for fragment/union "pieces" of RGBLINK sections 2025-10-05 15:07:25 -04:00
Rangi
7733ccdeb6 Implement __SCOPE__ (#1845) 2025-10-04 16:41:21 -04:00
Rangi
13e85b5151 Replace all ctype.h functions with locale-independent ones 2025-10-03 12:52:24 -04:00
Rangi
268b586c9d Release v1.0.0-rc2 2025-09-30 18:56:00 -04:00
Rangi
85d3b5df58 Add more RGBFIX tests 2025-09-30 18:20:53 -04:00
Rangi
eea277ae9c Add more tests for RGBFIX 2025-09-29 22:43:16 -04:00
Rangi
538395b92c Prevent simple syntax highlighters from breaking on /* in a string 2025-09-29 11:11:40 -04:00
Rangi
3108fb5297 Rename gbz80.7 "CPU opcode reference" to "Game Boy CPU instruction reference" 2025-09-28 16:43:36 -04:00
Rangi42
f99591bf6f Link FORCE_COLOR and NO_COLOR 2025-09-25 14:06:44 -04:00
Rangi42
0297da4d4c Add more tests for RGBASM coverage 2025-09-25 13:30:30 -04:00
Rangi42
96b953fe51 Add a test case for overlapping IF/ENDC and REPT/ENDR
This trips an asserton in the Rust rewrite because of its different
conditional stack design
2025-09-25 11:57:56 -04:00
Rangi42
0670c03bc2 Add CLI tests for RGBASM 2025-09-25 11:57:56 -04:00
Rangi42
09ef5b7e06 Refactor error fix suggestions in fstk_RunMacro 2025-09-24 19:35:41 -04:00
Rangi42
d5bb462f25 Separate isLetter into isUpper and isLower 2025-09-24 19:19:50 -04:00
Rangi42
b0727e9779 Suggest removing space before colon to define a label instead of invoking a macro 2025-09-24 18:32:45 -04:00
Rangi42
ca4b890273 Consistently do & alignMask, not % alignSize
Also add more unrelated tests for coverage
2025-09-23 13:25:51 -04:00
Rangi42
96a75500d3 Test gb-starter-kit from codeberg.org, not github.com 2025-09-23 13:06:12 -04:00
Rangi
634fd853d1 Factor out a single parseNumber utility function (#1839) 2025-09-22 15:15:24 -04:00
Rangi42
c8d22d8744 Refactor some iterator template code in RGBGFX pal_packing.cpp 2025-09-21 10:59:59 -04:00
Rangi42
3ece61b103 Use fewer templates in RGBGFX pal_packing.cpp 2025-09-20 22:00:17 -04:00
Rangi42
a82fd17529 Simplify RGBGFX code by using fewer templates 2025-09-20 21:06:51 -04:00
Rangi42
e7f5ab3f55 Warn about rgbgfx -O without -o or any of -A -T -P -Q 2025-09-20 20:23:29 -04:00
Rangi42
595c87b2f8 Update test dependencies 2025-09-20 15:23:02 -04:00
Rangi
c49a7d1e2f Make CLI and OPT options -p and -Q more consistent (#1834) 2025-09-20 13:54:28 -04:00
Rangi42
d8aff148bb Factor out RRANGE macro like RANGE 2025-09-19 16:53:44 -04:00
Rangi
e31bcabbaa Implement === and !== string comparison operators (#1832) 2025-09-19 14:06:36 -04:00
Rangi
67741ab428 Trim left space from macro args, even past block comments (#1831) 2025-09-19 13:44:18 -04:00
Rangi
e0a6199f83 Allow charmap to map 'characters' as well as "strings" (#1830) 2025-09-16 12:51:07 +02:00
Rangi42
6cffd991f7 Document rgbasm -Wempty-data-directive
Fixes #1829
2025-09-15 09:44:50 -04:00
Rangi42
1fdeb34e50 Fix too-short .out.bin expected test results
RGBASM tests now use `rgblink -x` instead of `dd` to get trimmed
test output. RGBLINK tests cannot do so because some of them
(e.g. bank-numbers.asm and sizeof-startof.asm) rely on ROMX
sections above 1, and `-x` implies `-t` which breaks that.
2025-09-15 09:25:33 -04:00
Rangi42
9f16881d64 Use for-each loop 2025-09-10 21:18:33 -04:00
Rangi
223b3d1921 More accurate 8-bit <=> 5-bit RGB color conversion (#1827) 2025-09-08 15:13:25 -04:00
Rangi
65d408eb5d Document more about the RGBDS architecture (#1809) 2025-09-07 15:34:52 -04:00
ReiquelApplegate
3677ab2ebf Correct -q's arg name in rgbgfx(1) (#1826) 2025-09-07 18:28:37 +02:00
Rangi42
d404621e0d Don't silence strings-as-numbers with OPT Wno-obsolete in tests 2025-09-07 12:17:39 -04:00
Rangi42
c8a088f281 Avoid nonessential EQUS expansion in tests 2025-09-06 13:45:12 -04:00
Rangi42
94ed28acf8 Avoid nonessential strings-as-numbers in tests 2025-09-06 12:45:01 -04:00
Rangi42
00b5077b2a Make usage info output responsive to the terminal width 2025-09-05 21:58:17 -04:00
Rangi42
024b33b63a make format should apply to all .cpp and .hpp files 2025-09-05 21:05:58 -04:00
Rangi42
dcc10cebc3 Add test for duplicate section after use of fragment literal
Verifies that #1822 was fixed (by c6997fe73c)
2025-09-05 20:14:01 -04:00
Rangi42
1fc9ba86c4 Some RGBLINK refactoring
- Consistently refer to `Section` fragments/unions as "pieces" (renaming `.nextu`)
- Remove `Symbol`'s `.label()` accessors (use `std::get<Label>`)
- Move some `Label`-related logic into `Symbol` methods
2025-09-05 16:34:51 -04:00
Rangi42
e569e0c200 Don't comment "; Next fragment/union" in .map files for empty section pieces
Fixes #1821
2025-09-05 15:31:46 -04:00
Rangi42
f7fb3af615 Run make tidy with Checks: '-*,misc-include-cleaner' in .clang-tidy (IWYU) 2025-09-04 13:39:23 -04:00
Rangi42
1dfc1d3231 Factor out isBinDigit and parseHexDigit utility functions 2025-09-04 13:23:10 -04:00
Rangi42
891e6f98df Fix formatting of very long fixed-point numbers 2025-09-04 12:54:14 -04:00
Rangi42
bdc885bd69 Avoid UB when checking truncation range
Fixes #1818
2025-09-04 12:04:10 -04:00
Rangi42
5b67dc94b6 Add more test coverage 2025-09-04 01:29:50 -04:00
Rangi42
4f702a4be8 Try to optimize RPN expressions with .emplace_back instead of .push_back 2025-09-03 23:00:06 -04:00
Rangi42
c5d437ab3c Tell people to use character literals or CHARVAL instead of strings as numbers 2025-09-03 22:46:19 -04:00
Rangi42
c5c2800f17 Move RPN buffer encoding logic into rpn.cpp 2025-09-03 22:36:00 -04:00
Rangi42
c798500563 Don't call rpn.clear() when we can safely assume it's already empty() 2025-09-03 21:02:57 -04:00
Rangi
590d113e94 Use a vector of RPN values (#1820)
This is instead of byte-encoding them in a different way than the actual object output's RPN buffer
2025-09-03 14:42:37 -04:00
Rangi
ee1db0a582 Fix RPN patches for all commands (#1819) 2025-09-02 16:44:25 -04:00
Rangi42
5701d747d4 Document the stricter rules for lexing underscores in integer constants 2025-09-02 14:39:28 -04:00
Rangi42
2110aaca20 Fix RGBLINK and RGBGFX warning/error message colors 2025-09-01 20:51:33 -04:00
Rangi42
8df88f92ba Release v1.0.0-rc1 2025-09-01 18:09:42 -04:00
Rangi
534a4efee4 Add 0/1/2 warning levels to rgblink -Wtruncation (#1816) 2025-09-01 15:35:53 -04:00
Rangi42
cc96b4d517 Two small improvements
- Check whether `.read()` completed
- `.reserve()` expected space ahead of time
2025-09-01 11:46:41 -04:00
Rangi42
0ccdbf509a Simplify format specs to not use a per-character state machine 2025-08-30 12:23:01 -04:00
Rangi
531278961f Require underscores to actually be digit separators (#1812)
Multiple, trailing, or next to decimal point are errors
2025-08-30 10:44:20 -04:00
Rangi
85176ef10a Fix q format spec (#1811) 2025-08-29 14:23:49 -04:00
Rangi
02b880e1b0 Separate RGBFIX header fixing from CLI option parsing (#1808) 2025-08-28 12:28:08 -04:00
Rangi42
c6997fe73c Factor out InsertionOrderedMap to group an indexed list with a string-keyed map 2025-08-26 16:04:45 -04:00
Rangi42
c578a7b761 More specific error message when an expression is not constant because a symbol is undefined 2025-08-24 22:02:41 -04:00
Rangi42
8564df51e5 -Wexport-undefined warning for exporting undefined symbols 2025-08-24 17:36:47 -04:00
Rangi42
62d3b44768 Add test for UNION without NEXTU 2025-08-24 13:08:27 -04:00
Rangi42
ead5337fe0 Use scoped blocks for case-specific variables 2025-08-23 21:52:22 -04:00
Rangi42
0d509aa65c Suggest DEF when undefined macros look like definitions 2025-08-23 21:40:51 -04:00
Rangi42
fcfc931867 Shorten lexing of simple tokens 2025-08-20 22:37:03 -04:00
Rangi
3d155d5695 Some refactoring and cleanup (#1806)
* Use clang-tidy `misc-include-cleaner` for IWYU `#include` cleanup

* Use `std::optional<size_t>` instead of `ssize_t`

* Rename some functions in linkdefs.hpp

* Fix header order
2025-08-20 16:09:04 -04:00
Rangi42
92ed6ece53 Support 32-bit addresses ("XL4") as of SDCC 4.4.0 2025-08-20 09:17:45 -04:00
Rangi42
e0e9ef190a Clarify DAA documentation 2025-08-20 08:57:32 -04:00
Rangi42
386fb5f398 Add more character utility functions 2025-08-19 19:17:40 -04:00
Rangi42
94e9ef5213 Factor shared code out of lexer_CaptureRept and lexer_CaptureMacro 2025-08-19 18:11:50 -04:00
Rangi
0c4c25b2d2 Add a little more test coverage (#1805)
Format main.cpp files more consistently

Add `make format` to run clang-format on everything
2025-08-19 15:26:23 -04:00
Rangi42
f7167d8115 More consistent documentation for NP-complete heuristics 2025-08-18 21:45:26 -04:00
Rangi
b7e0783ae7 Implement ? suffix to "quiet" a context and exclude it from backtraces (#1800) 2025-08-18 21:34:58 -04:00
Rangi42
77a105e189 Mention the "first-fit bin packing" algorithm of RGBLINK 2025-08-18 11:51:01 -04:00
Rangi42
a16dcc0ea5 Downgrade FreeBSD release used for testing from 15 to 14.3
See https://github.com/vmactions/freebsd-vm/issues/108

We get the error:

ld-elf.so.1: Shared object "libutil.so.10" not found, required by "pkg"
2025-08-17 14:09:39 -04:00
Rangi42
9f373d49ac Add more tests for edge-case macro and interpolation expansion behavior
Fixes #1803
2025-08-17 13:52:58 -04:00
Rangi
272019beb0 Fix line numbers from nested expansions (#1802) 2025-08-14 11:13:50 -04:00
Rangi
db6793f444 Don't count single quote ' as garbage (#1801)
Also copy the "blank space" (space or tab) vs "whitespace" (space,
tab, or newline) convention from `<ctype.h>`
2025-08-14 10:10:59 -04:00
Rangi42
ea1358bbe6 Predef std::pair to two-element std::tuple 2025-08-13 20:48:54 -04:00
Rangi42
2bdf61da70 Increase RGBASM test coverage 2025-08-13 12:26:01 -04:00
Rangi42
92826a726a Move some static variables into the only functions that use them 2025-08-13 09:34:49 -04:00
Rangi
9c3ce69180 Factor out shared backtrace code (#1793) 2025-08-12 17:56:54 -04:00
Rangi
50d0b101c3 Format linker script error backtraces the same way as others (#1792) 2025-08-12 16:58:17 -04:00
Rangi42
1bf1219e07 Factor out shared --color-parsing code 2025-08-12 15:53:57 -04:00
Rangi
7b405513d9 Make quote marks consistent in error/warning messages (#1791)
- "Double quotes" for strings (filenames, section names, CLI option arguments, etc)
- 'Single quotes' for characters and CLI option flags
- `Backticks` for keywords and identifiers (symbol names, charmap names, etc)

CLI option flags also have their leading dashes
2025-08-12 15:24:21 -04:00
Rangi
7df9c12a6c Fix division and modulo for very large negative numbers (#1790) 2025-08-11 20:46:47 -04:00
Rangi42
30a8503dcd Format RGBFIX and RGBGFX warnings/errors the same way as RGBASM and RGBLINK 2025-08-11 15:16:00 -04:00
Rangi42
02310489c6 Suggest ld and call when failing to link ldh and rst 2025-08-11 15:02:18 -04:00
Rangi
5f8b7474b4 Add -B/--backtrace option to RGBASM and RGBLINK (#1787) 2025-08-11 14:30:14 -04:00
Rangi
92a9c73ee7 Deprecate __DATE__ and __TIME__ (#1786) 2025-08-11 09:48:18 -04:00
Rangi42
7ade3e74b3 Avoid the need to repeat -Weverything in test .flags 2025-08-11 08:11:32 -04:00
Rangi
978e832914 Allow :: to join instructions *and* data declarations (#1785) 2025-08-11 08:04:42 -04:00
Rangi42
2130a5ba1f Error messages refer to "undefined" symbols and sections 2025-08-08 19:47:42 -04:00
Rangi42
9fc83efe06 Make rgbasm -Wlarge-constant enabled by default 2025-08-08 19:00:13 -04:00
Rangi
e41ce49698 Only print one warning for too-large integer constants, not one per digit (#1781)
This also makes all too-large integer constants evaluate to 0.
2025-08-08 18:58:38 -04:00
Rangi42
1574b5b1f7 Test the behavior of ds N, @ 2025-08-07 09:41:33 -04:00
Rangi42
3f6db080b4 Ensure that alignment is at most 16 and we do not cause UB 2025-08-06 15:31:30 -04:00
Rangi42
f9a55bd5cd Allow OPT to accept optional dashes before flags 2025-08-06 10:19:01 -04:00
Rangi
a4a830776b Deprecate treating strings as numbers (#1780) 2025-08-06 10:13:22 -04:00
Rangi42
34d99b273c Add zsh completions for --color flag 2025-08-06 09:26:22 -04:00
Rangi42
feb8365812 Document the deprecated rgbfix -O and string functions 2025-08-06 09:07:35 -04:00
Rangi42
bf66e346f0 Add -Wobsolete to RGBFIX and RGBGFX, and deprecate rgbfix -O 2025-08-05 17:18:54 -04:00
Rangi42
3a0a4b7f90 Deprecate 1-indexed string functions 2025-08-05 16:58:06 -04:00
Rangi
39f0f9edc0 Remove previously deprecated features (#1777)
- Treating multi-unit strings as numbers
- `rgbasm -Wnumeric-string`
- `ldio [c], a` and `ldio a, [c]` (use `ldh`)
- `ld [c], a` and `ld a, [c]` (use `ldh`)
- `ldh [$xx], a` and `ldh a, [$xx]` (use `$FFxx`)
2025-08-05 16:24:10 -04:00
Rangi42
a3983b7b0f Support rgbgfx -c dmg to imply -c dmg=e4
Fixes #1776
2025-08-05 14:18:25 -04:00
Rangi42
504a45a4ed Reuse isWhitespace and isNewline, also refactoring readAtFile 2025-08-05 13:46:53 -04:00
Rangi42
98c5c7f776 Support rgbgfx -c auto for automatic palette generation 2025-08-05 13:05:21 -04:00
Rangi42
7020cf7188 Make struct Uppercase have constexpr methods 2025-08-05 12:11:23 -04:00
Rangi42
70d129fcd7 Color "warning:" yellow in warnx 2025-08-05 11:26:21 -04:00
Rangi42
2d5f4d8910 More consistent missing-input error messages 2025-08-05 11:22:58 -04:00
Rangi42
26c6911c8f More consistent man pages 2025-08-05 11:16:00 -04:00
Rangi42
f1fd3abcff Don't bother searching the keywordDict for local labels 2025-08-05 10:49:09 -04:00
Rangi42
2cae47a5a2 Color verbose output as magenta
Output RGBASM's lexed tokens at level 5 (TRACE)
2025-08-05 00:00:57 -04:00
Rangi42
ac75a085fa Refactor lexer_CaptureRept and lexer_CaptureMacro 2025-08-04 23:26:29 -04:00
Rangi42
0c1b422c36 Refactor some redundant lexer code 2025-08-04 22:43:34 -04:00
Rangi
23ce888d65 Use colored/styled text output for diagnostics and usage info (#1775) 2025-08-04 17:02:24 -04:00
Rangi42
d992b21141 Fix man page syntax for Flags 2025-08-03 13:22:22 -04:00
Rangi42
903492cce2 Fully support rgbasm -MC in Bash completion script 2025-08-03 13:08:36 -04:00
Rangi42
fc9b614225 Allow the index of CHARVAL to be optional
Fixes #1773
2025-08-03 08:44:06 -04:00
Rangi42
543b7fa6c2 Use std::nothrow for the only new expression
Ideally we'd use `std::make_shared`, but it has insufficient compiler
support for array types
2025-08-02 22:52:20 -04:00
Rangi42
5a7ffd19b0 Move or remove some redundant information from the README
* Move the folder structure to ARCHITECTURE.md (which can be
  expanded as per #707)

* Move the acknowledgements to CONTRIBUTORS.md

* Remove the history (it's a copy of man/rgbds.7)
2025-08-02 22:09:09 -04:00
Rangi42
7be7483571 Move more RGBASM parser action code out of the Bison file 2025-08-02 21:38:13 -04:00
Rangi42
2eeb333be0 Factor out more shared math operations between RGBASM and RGBLINK 2025-08-02 19:55:44 -04:00
Rangi42
fe8baaec50 Show contributors' avatars with contrib.rocks 2025-08-02 17:12:29 -04:00
Rangi
752b273aec Extend RGBASM and RGBLINK verbosity flags to have multiple levels like RGBGFX (#1772) 2025-08-02 17:10:10 -04:00
Rangi42
b51056f743 Fix "while expanding symbol" error message
Fixes #1771
2025-08-01 16:25:44 -04:00
Rangi42
ee1aed51d8 Use std::numbers::pi instead of nonstandard M_PI 2025-08-01 13:07:02 -04:00
Rangi42
db2cda790c Factor out a single consumeChar function
This and `peek` are the only functions that should call the low-level
`peekChar` and `peekCharAhead` methods.
2025-08-01 12:12:22 -04:00
Rangi42
86b43ea14f Fix interpolation after empty string literal 2025-07-31 15:17:11 -04:00
842 changed files with 12207 additions and 8583 deletions

View File

@@ -71,6 +71,7 @@ Language: Cpp
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
PPIndentWidth: -1
PenaltyBreakScopeResolution: 1000
PointerAlignment: Right
QualifierAlignment: Right
ReflowComments: true

View File

@@ -2,7 +2,7 @@
root = true
indent_style = tab
indent_size = tab
tab_width = 8
tab_width = 4
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -151,7 +151,7 @@ jobs:
body: |
Please ensure that the packages below work properly.
Once that's done, replace this text with the changelog, un-draft the release, and update the `release` branch.
By the way, if you forgot to update `include/version.hpp`, RGBASM's version test is gonna fail in the tag's regression testing! (Use `git push --delete origin <tag>` to delete it)
By the way, if you forgot to update `include/version.hpp`, RGBASM's version test is going to fail in the tag's regression testing! (Use `git push --delete origin <tag>` to delete it)
draft: true # Don't publish the release quite yet...
prerelease: ${{ contains(github.ref, '-rc') }}
files: |

View File

@@ -373,7 +373,7 @@ jobs:
- name: Build & test using CMake on FreeBSD
uses: vmactions/freebsd-vm@v1
with:
release: "15.0"
release: "14.3"
usesh: true
prepare: |
pkg install -y \

279
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,279 @@
# RGBDS Architecture
The RGBDS package consists of four programs: RGBASM, RGBLINK, RGBFIX, and RGBGFX.
- RGBASM is the assembler. It takes assembly code as input, and produces an RGB object file as output (and optionally a state file, logging the final state of variables and constants).
- RGBLINK is the linker. It takes object files as input, and produces a ROM file as output (and optionally a symbol and/or map file, logging where the assembly declarations got placed in the ROM).
- RGBFIX is the checksum/header fixer. It takes a ROM file as input, and outputs the same ROM file (or modifies it in-place) with the cartridge header's checksum and other metadata fixed for consistency.
- RGBGFX is the graphics converter. It takes a PNG image file as input, and outputs the tile data, palettes, tilemap, attribute map, and/or palette map in formats that the Game Boy can use.
In the simplest case, a single pipeline can turn an assembly file into a ROM:
```console
(rgbasm -o - - | rgblink -o - - | rgbfix -v -p 0) < game.asm > game.gb
```
This document describes how these four programs are structured. It goes over each source code file, noting which data is *global* (and thus scoped in all files), *owned* by that file (i.e. that is where the data's memory is managed, via [RAII](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization)) or *referenced* by that file (i.e. there are non-owning pointers to some data, and care must be taken to not dereference those pointers after the data's owner has moved or deleted the data).
We assume that the programs are single-threaded; data structures and operations may not be thread-safe.
## Folder Organization
The RGBDS source code file structure is as follows:
```
rgbds/
├── .github/
│ ├── scripts/
│ │ └── ...
│ └── workflows/
│ └── ...
├── contrib/
│ ├── bash_compl/
│ ├── zsh_compl/
│ │ └── ...
│ └── ...
├── include/
│ └── ...
├── man/
│ └── ...
├── src/
│ ├── asm/
│ │ └── ...
│ ├── extern/
│ │ └── ...
│ ├── fix/
│ │ └── ...
│ ├── gfx/
│ │ └── ...
│ ├── link/
│ │ └── ...
│ ├── CMakeLists.txt
│ ├── bison.sh
│ └── ...
├── test/
│ ├── fetch-test-deps.sh
│ ├── run-tests.sh
│ └── ...
├── .clang-format
├── .clang-tidy
├── CMakeLists.txt
├── compile_flags.txt
├── Dockerfile
└── Makefile
```
- **`.github/`:**
Files related to the integration of the RGBDS codebase with GitHub features.
* **`scripts/`:**
Scripts used by GitHub Actions workflow files.
* **`workflows/`:**
GitHub Actions CI workflow description files. Used for automated testing, deployment, etc.
- **`contrib/`:**
Scripts and other resources which may be useful to RGBDS users and developers.
* **`bash_compl/`:**
Tab completion scripts for use with `bash`. Run them with `source` somewhere in your `.bashrc`, and they should auto-load when you open a shell.
* **`zsh_compl/`:**
Tab completion scripts for use with `zsh`. Put them somewhere in your `fpath`, and they should auto-load when you open a shell.
- **`include/`:**
Header files for the respective source files in `src`.
- **`man/`:**
Manual pages to be read with `man`, written in the [`mandoc`](https://mandoc.bsd.lv) dialect.
- **`src/`:**
Source code of RGBDS.
* **`asm/`:**
Source code of RGBASM.
* **`extern/`:**
Source code copied from external sources.
* **`fix/`:**
Source code of RGBFIX.
* **`gfx/`:**
Source code of RGBGFX.
* **`link/`:**
Source code of RGBLINK.
* **`CMakeLists.txt`:**
Defines how to build individual RGBDS programs with CMake, including the source files that each program depends on.
* **`bison.sh`:**
Script used to run the Bison parser generator with the latest flags that the user's version supports.
- **`test/`:**
Testing framework used to verify that changes to the code don't break or modify the behavior of RGBDS.
* **`fetch-test-deps.sh`:**
Script used to fetch dependencies for building external repositories. `fetch-test-deps.sh --help` describes its options.
* **`run-tests.sh`:**
Script used to run tests, including internal test cases and external repositories. `run-tests.sh --help` describes its options.
- **`.clang-format`:**
Code style for automated C++ formatting with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html) (for which we define the shortcut `make format`).
- **`.clang-tidy`:**
Configuration for C++ static analysis with [`clang-tidy`](https://clang.llvm.org/extra/clang-tidy/) (for which we define the shortcut `make tidy`).
- **`CMakeLists.txt`:**
Defines how to build RGBDS with CMake.
- **`compile_flags.txt`:**
Compiler flags for `clang-tidy`.
- **`Dockerfile`:**
Defines how to build RGBDS with Docker (which we do in CI to provide a [container image](https://github.com/gbdev/rgbds/pkgs/container/rgbds)).
- **`Makefile`:**
Defines how to build RGBDS with `make`, including the source files that each program depends on.
## RGBDS
These files in the `src/` directory are shared across multiple programs: often all four (RGBASM, RGBLINK, RGBFIX, and RGBGFX), sometimes only RGBASM and RGBLINK.
- **`backtrace.cpp`:**
Generic printing of location backtraces for RGBASM and RGBLINK. Allows configuring backtrace styles with a command-line flag (conventionally `-B/--backtrace`). Renders warnings in yellow, errors in red, and locations in cyan.
- **`cli.cpp`:**
A function for parsing command-line options, including RGBDS-specific "at-files" (a filename containing more options, prepended with an "`@`").
This is the only file to use the extern/getopt.cpp variables and functions.
- **`diagnostics.cpp`:**
Generic warning/error diagnostic support for all programs. Allows command-line flags (conventionally `-W`) to have `no-`, `error=`, or `no-error=` prefixes, and `=` level suffixes; allows "meta" flags to affect groups of individual flags; and counts how many total errors there have been. Every program has its own `warning.cpp` file that uses this.
- **`linkdefs.cpp`:**
Constants, data, and functions related to RGBDS object files, which are used for RGBASM output and RGBLINK input.
This file defines two *global* variables, `sectionTypeInfo` (metadata about each section type) and `sectionModNames` (names of section modifiers, for error reporting). RGBLINK may change some values in `sectionTypeInfo` depending on its command-line options (this only affects RGBLINK; `sectionTypeInfo` is immutable in RGBASM).
- **`opmath.cpp`:**
Functions for mathematical operations in RGBASM and RGBLINK that aren't trivially equivalent to built-in C++ ones, such as division and modulo with well-defined results for negative values.
- **`style.cpp`:**
Generic printing of cross-platform colored or bold text. Obeys the [`FORCE_COLOR`](https://force-color.org/) and [`NO_COLOR`](https://no-color.org/) environment variables, and allows configuring with a command-line flag (conventionally `--color`).
- **`usage.cpp`:**
Generic printing of usage information. Renders headings in green, flags in cyan, and URLs in blue. Every program has its own `main.cpp` file that uses this.
- **`util.cpp`:**
Utility functions applicable to most programs, mostly dealing with text strings, such as locale-independent character checks.
- **`verbosity.cpp`:**
Generic printing of messages conditionally at different verbosity levels. Allows configuring with a command-line flag (conventionally `-v/--verbose`).
- **`version.cpp`:**
RGBDS version number and string for all the programs.
## External
These files have been copied ("vendored") from external authors and adapted for use with RGBDS. Both of our vendored dependencies use the same MIT license as RGBDS.
- **`getopt.cpp`:**
Functions for parsing command-line options, including conventional single-dash and double-dash options.
This file defines some *global* `musl_opt*` variables, including `musl_optarg` (the argument given after an option flag) and `musl_optind` (the index of the next option in `argv`). Copied from [musl libc](https://musl.libc.org/).
- **`utf8decoder.cpp`:**
Function for decoding UTF-8 bytes into Unicode code points. Copied from [Björn Höhrmann](https://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
## RGBASM
- **`actions.cpp`:**
Actions taken by the assembly language parser, to avoid large amounts of code going in the parser.y file.
- **`charmap.cpp`:**
Functions and data related to charmaps.
This file *owns* the `Charmap`s in its `charmaps` collection. It also maintains a static `currentCharmap` pointer, and a `charmapStack` stack of pointers to `Charmap`s within `charmaps` (which is affected by `PUSHC` and `POPC` directives).
- **`fixpoint.cpp`:**
Functions for fixed-point math, with configurable [Q*m*.*n*](https://en.wikipedia.org/wiki/Q_(number_format)) precision.
- **`format.cpp`:**
`FormatSpec` methods for parsing and applying format specs, as used by `{interpolations}` and `STRFMT`.
- **`fstack.cpp`:**
Functions and data related to "fstack" nodes (the contents of top-level or `INCLUDE`d files, macro expansions, or `REPT`/`FOR` loop iterations) and their "contexts" (metadata that is only relevant while a node's content is being lexed and parsed).
This file *owns* the `Context`s in its `contextStack` collection. Each of those `Context`s *owns* its `LexerState`, and *refers* to its `FileStackNode`, `uniqueIDStr`, and `macroArgs`. Each `FileStackNode` also *references* its `parent`.
- **`lexer.cpp`:**
Functions and data related to [lexing](https://en.wikipedia.org/wiki/Lexical_analysis) assembly source code into tokens, which can then be parsed.
This file maintains static `lexerState` and `lexerStateEOL` pointers to `LexerState`s from the `Context`s in `fstack.cpp`.
Each `LexerState` *owns* its `content` and its `expansions`' content. Each `Expansion` (the contents of an `{interpolation}` or macro argument) in turn *owns* its `contents`.
The lexer and parser are interdependent: when the parser reaches certain tokens, it changes the lexer's mode, which affects how characters get lexed into tokens. For example, when the parser reaches a macro name, it changes the lexer to "raw" mode, which lexes the rest of the line as a sequence of string arguments to the macro.
- **`macro.cpp`:**
`MacroArgs` methods related to macro arguments. Each `MacroArgs` *references* its arguments' contents.
- **`main.cpp`:**
The `main` function for running RGBASM, including the initial handling of command-line options.
This file defines a *global* `options` variable with the parsed CLI options.
- **`opt.cpp`:**
Functions for parsing options specified by `OPT` or by certain command-line options.
This file *owns* the `OptStackEntry`s in its `stack` collection (which is affected by `PUSHO` and `POPO` directives).
- **`output.cpp`:**
Functions and data related to outputting object files (with `-o/--output`) and state files (with `-s/--state`).
This file *owns* its `assertions` (created by `ASSERT` and `STATIC_ASSERT` directives). Every assertion gets output in the object file.
This file also *references* some `fileStackNodes`, and maintains static pointers to `Symbol`s in `objectSymbols`. Only the "registered" symbols and fstack nodes get output in the object file. The `fileStackNodes` and `objectSymbols` collections keep track of which nodes and symbols have been registered for output.
- **`parser.y`:**
Grammar for the RGBASM assembly language, which Bison preprocesses into a [LALR(1) parser](https://en.wikipedia.org/wiki/LALR_parser).
The Bison-generated parser calls `yylex` (defined in `lexer.cpp`) to get the next token, and calls `yywrap` (defined in `fstack.cpp`) when the current context is out of tokens and returns `EOF`.
- **`rpn.cpp`:**
`Expression` methods and data related to "[RPN](https://en.wikipedia.org/wiki/Reverse_Polish_notation)" expressions. When a numeric expression is parsed, if its value cannot be calculated at assembly time, it is built up into a buffer of RPN-encoded operations to do so at link time by RGBLINK. The valid RPN operations are defined in [man/rgbds.5](man/rgbds.5).
- **`section.cpp`:**
Functions and data related to `SECTION`s.
This file *owns* the `Section`s in its `sections` collection. It also maintains various static pointers to those sections, including the `currentSection`, `currentLoadSection`, and `sectionStack` (which is affected by `PUSHS` and `POPS` directives). (Note that sections cannot be deleted.)
- **`symbol.cpp`:**
Functions and data related to symbols (labels, constants, variables, string constants, macros, etc).
This file *owns* the `Symbol`s in its `symbols` collection, and the various built-in ones outside that collection (`PCSymbol` for "`@`", `NARGSymbol` for "`_NARG`", etc). It also maintains a static `purgedSymbols` collection to remember which symbol names have been `PURGE`d from `symbols`, for error reporting purposes.
- **`warning.cpp`:**
Functions and data for warning and error output.
This file defines a *global* `warnings` variable using the `diagnostics.cpp` code for RGBASM-specific warning flags.
## RGBFIX
- **`fix.cpp`:**
Functions for fixing the ROM header.
- **`main.cpp`:**
The `main` function for running RGBFIX, including the initial handling of command-line options.
This file defines a *global* `options` variable with the parsed CLI options.
- **`mbc.cpp`:**
Functions and data related to [MBCs](https://gbdev.io/pandocs/MBCs.html), including the names of known MBC values.
- **`warning.cpp`:**
Functions and data for warning and error output.
This file defines a *global* `warnings` variable using the `diagnostics.cpp` code for RGBFIX-specific warning flags.
## RGBGFX
- **`color_set.cpp`:**
`ColorSet` methods for creating and comparing sets of colors. A color set includes the unique colors used by a single tile, and these sets are then packed into palettes.
- **`main.cpp`:**
The `main` function for running RGBGFX, including the initial handling of command-line options.
This file defines a *global* `options` variable with the parsed CLI options.
- **`pal_packing.cpp`:**
Functions for packing color sets into palettes. This is done with an ["overload-and-remove" heuristic](https://arxiv.org/abs/1605.00558) for a pagination algorithm.
- **`pal_sorting.cpp`:**
Functions for sorting colors within palettes, which works differently for grayscale, RGB, or indexed-color palettes.
- **`pal_spec.cpp`:**
Functions for parsing various formats of palette specifications (from `-c/--colors`).
- **`palette.cpp`:**
`Palette` methods for working with up to four GBC-native (RGB555) colors.
- **`png.cpp`:**
`Png` methods for reading PNG image files, standardizing them to 8-bit RGBA pixels while also reading their indexed palette if there is one.
- **`process.cpp`:**
Functions related to generating and outputting files (tile data, palettes, tilemap, attribute map, and/or palette map).
- **`reverse.cpp`:**
Functions related to reverse-generating RGBGFX outputs into a PNG file (for `-r/--reverse`).
- **`rgba.cpp`:**
`Rgba` methods related to RGBA colors and their 8-bit or 5-bit representations.
- **`warning.cpp`:**
Functions and data for warning and error output.
This file defines a *global* `warnings` variable using the `diagnostics.cpp` code for RGBGFX-specific warning flags.
## RGBLINK
- **`assign.cpp`:**
Functions and data for assigning `SECTION`s to specific banks and addresses.
This file *owns* the `memory` table of free space: each section type is associated with a list of each bank's free address ranges, which are allocated to sections using a [first-fit decreasing](https://en.wikipedia.org/wiki/Bin_packing_problem#First-fit_algorithm) bin-packing algorithm.
- **`fstack.cpp`:**
Functions related to "fstack" nodes (the contents of top-level or `INCLUDE`d files, macro expansions, or `REPT`/`FOR` loop iterations) read from the object files. At link time, these nodes are only needed for printing of location backtraces.
- **`layout.cpp`:**
Actions taken by the linker script parser, to avoid large amounts of code going in the script.y file.
This file maintains some static data about the current bank and address layout, which get checked and updated for consistency as the linker script is parsed.
- **`lexer.cpp`:**
Functions and data related to [lexing](https://en.wikipedia.org/wiki/Lexical_analysis) linker script files into tokens, which can then be parsed.
This file *owns* the `LexerStackEntry`s in its `lexerStack` collection. Each of those `LexerStackEntry`s *owns* its `file`. The stack is updated as linker scripts can `INCLUDE` other linker script pieces.
The linker script lexer is simpler than the RGBASM one, and does not have modes.
- **`main.cpp`:**
The `main` function for running RGBLINK, including the initial handling of command-line options.
This file defines a *global* `options` variable with the parsed CLI options.
- **`object.cpp`:**
Functions and data for reading object files generated by RGBASM.
This file *owns* the `Symbol`s in its `symbolLists` collection, and the `FileStackNode`s in its `nodes` collection.
- **`output.cpp`:**
Functions and data related to outputting ROM files (with `-o/--output`), symbol files (with `-n/--sym`), and map files (with `-m/--map`).
This file *references* some `Symbol`s and `Section`s, in collections that keep them sorted by address and name, which allows the symbol and map output to be in order.
- **`patch.cpp`:**
Functions and data related to "[RPN](https://en.wikipedia.org/wiki/Reverse_Polish_notation)" expression patches read from the object files, including the ones for `ASSERT` conditions. After sections have been assigned specific locations, the RPN patches can have their values calculated and applied to the ROM. The valid RPN operations are defined in [man/rgbds.5](man/rgbds.5).
This file *owns* the `Assertion`s in its `assertions` collection, and the `RPNStackEntry`s in its `rpnStack` collection.
- **`script.y`:**
Grammar for the linker script language, which Bison preprocesses into a [LALR(1) parser](https://en.wikipedia.org/wiki/LALR_parser).
The Bison-generated parser calls `yylex` (defined in `lexer.cpp`) to get the next token, and calls `yywrap` (also defined in `lexer.cpp`) when the current context is out of tokens and returns `EOF`.
- **`sdas_obj.cpp`:**
Functions and data for reading object files generated by [GBDK with SDCC](https://gbdk.org/). RGBLINK support for these object files is incomplete.
- **`section.cpp`:**
Functions and data related to `SECTION`s read from the object files.
This file *owns* the `Section`s in its `sections` collection.
- **`symbol.cpp`:**
Functions and data related to symbols read from the object files.
This file *references* the `Symbol`s in its `symbols` and `localSymbols` collections, which allow accessing symbols by name.
- **`warning.cpp`:**
Functions and data for warning and error output.
This file defines a *global* `warnings` variable using the `diagnostics.cpp` code for RGBLINK-specific warning flags.

View File

@@ -51,8 +51,8 @@ but doesn't know that there's someone working on it.
Note that you must contribute all your changes under the MIT License. If you are
just modifying a file, you don't need to do anything (maybe update the copyright
years). If you are adding new files, you need to use the `SPDX-License-Identifier: MIT`
header.
years). If you are adding new files, you need to use the
`SPDX-License-Identifier: MIT` header.
1. Fork this repository.
2. Checkout the `master` branch.
@@ -63,71 +63,117 @@ header.
new warning (but it may be possible to remove some warning checks if it makes
the code much easier).
5. Test your changes by running `./run-tests.sh` in the `test` directory.
(You must run `./fetch-test-deps.sh` first; if you forget to, the test suite will fail and remind you mid-way.)
(You must run `./fetch-test-deps.sh` first; if you forget to, the test suite
will fail and remind you mid-way.)
5. Format your changes according to `clang-format`, which will reformat the
coding style according to our standards defined in `.clang-format`.
6. Create a pull request against the branch `master`.
7. Be prepared to get some comments about your code and to modify it. Tip: Use
7. Check the results of the GitHub Actions CI jobs for your pull request. The
"Code format checking" and "Regression testing" jobs should all succeed.
The "Diff completeness check" and "Static analysis" jobs should be manually
checked, as they may output warnings which do not count as failure errors.
The "Code coverage report" provides an
[LCOV](https://github.com/linux-test-project/lcov)-generated report which
can be downloaded and checked to see if your new code has full test coverage.
8. Be prepared to get some comments about your code and to modify it. Tip: Use
`git rebase -i origin/master` to modify chains of commits.
## Adding a test
The test suite is a little ad-hoc, so the way tests work is different for each program being tested.
The test suite is a little ad-hoc, so the way tests work is different for each
program being tested.
Feel free to modify how the test scripts work, if the thing you want to test doesn't fit the existing scheme(s).
Feel free to modify how the test scripts work, if the thing you want to test
doesn't fit the existing scheme(s).
### RGBASM
There are two kinds of test.
#### Simple tests
Each `.asm` file corresponds to one test.
RGBASM will be invoked on the `.asm` file with all warnings enabled.
If a `.out` file exists, RGBASM's output (`print`, `println`, etc.) must match its contents.
If a `.err` file exists, RGBASM's error output (`warn`, errors, etc.) must match its contents.
If a `.flags` file exists, its first line contains flags to pass to RGBASM.
(There may be more lines, which will be ignored; they can serve as comments to
explain what the test is about.)
If a `.out.bin` file exists, the object file will be linked, and the generated ROM truncated to the length of the `.out.bin` file.
If a `.out` file exists, RGBASM's output (`print`, `println`, etc.) must match
its contents.
If a `.err` file exists, RGBASM's error output (`warn`, errors, etc.) must match
its contents.
If a `.out.bin` file exists, the object file will be linked, and the generated
ROM truncated to the length of the `.out.bin` file.
After that, the ROM must match the `.out.bin` file.
#### CLI tests
Each `.flags` file in `cli/` corresponds to one test.
RGBASM will be invoked, passing it the first line of the `.flags` file.
(There may be more lines, which will be ignored; they can serve as comments to
explain what the test is about.)
If a `.out` file exists, RGBASM's output (`print`, `println`, etc.) must match
its contents.
If a `.err` file exists, RGBASM's error output (`warn`, errors, etc.) must match
its contents.
### RGBLINK
Each `.asm` file corresponds to one test, or one *set* of tests.
All tests begin by assembling the `.asm` file into an object file, which will be linked in various ways depending on the test.
All tests begin by assembling the `.asm` file into an object file, which will be
linked in various ways depending on the test.
#### Simple tests
These simply check that RGBLINK's output matches some expected output.
A `.out` file **must** exist, and RGBLINK's total output must match that file's contents.
A `.out` file **must** exist, and RGBLINK's total output must match that file's
contents.
Additionally, if a `.out.bin` file exists, the `.gb` file generated by RGBLINK must match it.
Additionally, if a `.out.bin` file exists, the `.gb` file generated by RGBLINK
must match it.
#### Linker script tests
These allow applying various linker scripts to the same object file.
If one or more `.link` files exist, whose names start the same as the `.asm` file, then each of those files correspond to one test.
If one or more `.link` files exist, whose names start the same as the `.asm`
file, then each of those files correspond to one test.
Each `.link` linker script **must** be accompanied by a `.out` file, and RGBLINK's total output must match that file's contents when passed the corresponding linker script.
Each `.link` linker script **must** be accompanied by a `.out` file, and
RGBLINK's total output must match that file's contents when passed the
corresponding linker script.
#### Variant tests
These allow testing RGBLINK's `-d`, `-t`, and `-w` flags.
If one or more <code>-<var>&lt;flag&gt;</var>.out</code> or <code>-no-<var>&lt;flag&gt;</var>.out</code> files exist, then each of them corresponds to one test.
If one or more
<code>-<var>&lt;flag&gt;</var>.out</code> or <code>-no-<var>&lt;flag&gt;</var>.out</code>
files exist, then each of them corresponds to one test.
The object file will be linked with and without said flag, respectively; and in each case, RGBLINK's total output must match the `.out` file's contents.
The object file will be linked with and without said flag, respectively; and in
each case, RGBLINK's total output must match the `.out` file's contents.
### RGBFIX
Each `.flags` file corresponds to one test.
Each one is a text file whose first line contains flags to pass to RGBFIX.
(There may be more lines, which will be ignored; they can serve as comments to explain what the test is about.)
(There may be more lines, which will be ignored; they can serve as comments to
explain what the test is about.)
RGBFIX will be invoked on the `.bin` file if it exists, or else on default-input.bin.
RGBFIX will be invoked on the `.bin` file if it exists, or else on
default-input.bin.
If no `.out` file exist, RGBFIX is not expected to output anything.
If one *does* exist, RGBFIX's output **must** match the `.out` file's contents.
If no `.err` file exists, RGBFIX is simply expected to be able to process the file normally.
If one *does* exist, RGBFIX's return status is ignored, but its error output **must** match the `.err` file's contents.
If no `.err` file exists, RGBFIX is simply expected to be able to process the
file normally.
If one *does* exist, RGBFIX's return status is ignored, but its error output
**must** match the `.err` file's contents.
Additionally, if a `.gb` file exists, the output of RGBFIX must match the `.gb`.
@@ -139,20 +185,27 @@ There are three kinds of test.
Each `.png` file corresponds to one test.
RGBGFX will be invoked on the file.
If a `.flags` file exists, it will be used as part of the RGBGFX invocation (<code>@<var>&lt;file&gt;</var>.flags</code>).
If a `.flags` file exists, it will be used as part of the RGBGFX invocation
(<code>@<var>&lt;file&gt;</var>.flags</code>).
If `.out.1bpp`, `.out.2bpp`, `.out.pal`, `.out.tilemap`, `.out.attrmap`, or `.out.palmap` files exist, RGBGFX will create the corresponding kind of output, which must match the file's contents.
If `.out.1bpp`, `.out.2bpp`, `.out.pal`, `.out.tilemap`, `.out.attrmap`, or
`.out.palmap` files exist, RGBGFX will create the corresponding kind of output,
which must match the file's contents.
Multiple kinds of output may be tested for the same input.
If no `.err` file exists, RGBGFX is simply expected to be able to process the file normally.
If one *does* exist, RGBGFX's return status is ignored, but its output **must** match the `.err` file's contents.
If no `.err` file exists, RGBGFX is simply expected to be able to process the
file normally.
If one *does* exist, RGBGFX's return status is ignored, but its output **must**
match the `.err` file's contents.
#### Reverse tests
Each `.1bpp` or `.2bpp` file corresponds to one test.
RGBGFX will be invoked on the file with `-r 1` for reverse mode, then invoked on the output without `-r 1`.
RGBGFX will be invoked on the file with `-r 1` for reverse mode, then invoked on
the output without `-r 1`.
The round-trip output must match the input file's contents.
If a `.flags` file exists, it will be used as part of the RGBGFX invocation (<code>@<var>&lt;file&gt;</var>.flags</code>).
If a `.flags` file exists, it will be used as part of the RGBGFX invocation
(<code>@<var>&lt;file&gt;</var>.flags</code>).
#### Random seed tests
@@ -161,18 +214,24 @@ Each one is a binary RNG file which is passed to the `rgbgfx_test` program.
### Downstream projects
1. Make sure the downstream project supports <code>make <var>&lt;target&gt;</var> RGBDS=<var>&lt;path/to/RGBDS/&gt;</var></code>.
While the test suite supports any Make target name, only [Make](//gnu.org/software/make) is currently supported, and the Makefile must support a `RGBDS` variable to use a non-system RGBDS directory.
1. Make sure the downstream project supports
<code>make <var>&lt;target&gt;</var> RGBDS=<var>&lt;path/to/RGBDS/&gt;</var></code>.
While the test suite supports any Make target name, only
[Make](//gnu.org/software/make) is currently supported, and the Makefile must
support a `RGBDS` variable to use a non-system RGBDS directory.
Also, only projects hosted on GitHub are currently supported.
2. Add the project to `test/fetch-test-deps.sh`: add a new `action` line at the bottom, following the existing pattern:
2. Add the project to `test/fetch-test-deps.sh`: add a new `action` line at the
bottom, following the existing pattern:
```sh
action <owner> <repo> <date of last commit> <hash of last commit>
```
(The date is used to avoid fetching too much history when cloning the repositories.)
3. Add the project to `test/run-tests.sh`: add a new `test_downstream` line at the bottom, following the existing pattern:
(The date is used to avoid fetching too much history when cloning the
repositories.)
3. Add the project to `test/run-tests.sh`: add a new `test_downstream` line at
the bottom, following the existing pattern:
```sh
test_downstream <owner> <repo> <makefile target> <build file> <sha1 hash of build file>
@@ -180,11 +239,16 @@ Each one is a binary RNG file which is passed to the `rgbgfx_test` program.
## Container images
The CI will [take care](https://github.com/gbdev/rgbds/blob/master/.github/workflows/build-container.yml) of updating the [rgbds container](https://github.com/gbdev/rgbds/pkgs/container/rgbds) image tagged `master`.
The CI will
[take care](https://github.com/gbdev/rgbds/blob/master/.github/workflows/build-container.yml)
of updating the
[rgbds container](https://github.com/gbdev/rgbds/pkgs/container/rgbds) image
tagged `master`.
When a git tag is pushed, the image is also tagged with that tag.
The image can be built locally and pushed to the GitHub container registry by manually running:
The image can be built locally and pushed to the GitHub container registry by
manually running:
```bash
# e.g. to build and tag as 'master'

View File

@@ -32,3 +32,17 @@
- yenatch &lt;yenatch@gmail.com&gt;
- phs &lt;phil@philhsmith.com&gt;
- jidoc01 &lt;jidoc01@naver.com&gt;
[<img src="https://contrib.rocks/image?repo=gbdev/rgbds">](https://github.com/gbdev/rgbds/graphs/contributors)
Contributor image made with [contrib.rocks](https://contrib.rocks).
## Acknowledgements
RGBGFX generates palettes using algorithms found in the paper
["Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items"](https://arxiv.org/abs/1605.00558)
([GitHub](https://github.com/pagination-problem/pagination), MIT license),
by Aristide Grange, Imed Kacem, and Sébastien Martin.
RGBGFX's color palette was taken from [SameBoy](https://sameboy.github.io), with
permission and help by [LIJI](https://github.com/LIJI32).

View File

@@ -1,6 +1,6 @@
FROM debian:12-slim
LABEL org.opencontainers.image.source=https://github.com/gbdev/rgbds
ARG version=0.9.4
ARG version=1.0.0-rc2
WORKDIR /rgbds
COPY . .

View File

@@ -3,7 +3,7 @@
.SUFFIXES:
.SUFFIXES: .cpp .y .o
.PHONY: all clean install checkdiff develop debug profile coverage tidy iwyu mingw32 mingw64 wine-shim dist
.PHONY: all clean install checkdiff develop debug profile coverage format tidy iwyu mingw32 mingw64 wine-shim dist
# User-defined variables
@@ -51,8 +51,11 @@ all: rgbasm rgblink rgbfix rgbgfx
common_obj := \
src/extern/getopt.o \
src/cli.o \
src/diagnostics.o \
src/usage.o
src/style.o \
src/usage.o \
src/util.o
rgbasm_obj := \
${common_obj} \
@@ -72,15 +75,17 @@ rgbasm_obj := \
src/asm/symbol.o \
src/asm/warning.o \
src/extern/utf8decoder.o \
src/backtrace.o \
src/linkdefs.o \
src/opmath.o \
src/util.o
src/verbosity.o
src/asm/lexer.o src/asm/main.o: src/asm/parser.hpp
rgblink_obj := \
${common_obj} \
src/link/assign.o \
src/link/fstack.o \
src/link/lexer.o \
src/link/layout.o \
src/link/main.o \
@@ -93,14 +98,16 @@ rgblink_obj := \
src/link/symbol.o \
src/link/warning.o \
src/extern/utf8decoder.o \
src/backtrace.o \
src/linkdefs.o \
src/opmath.o \
src/util.o
src/verbosity.o
src/link/lexer.o src/link/main.o: src/link/script.hpp
rgbfix_obj := \
${common_obj} \
src/fix/fix.o \
src/fix/main.o \
src/fix/mbc.o \
src/fix/warning.o
@@ -112,12 +119,13 @@ rgbgfx_obj := \
src/gfx/pal_packing.o \
src/gfx/pal_sorting.o \
src/gfx/pal_spec.o \
src/gfx/palette.o \
src/gfx/png.o \
src/gfx/process.o \
src/gfx/reverse.o \
src/gfx/rgba.o \
src/gfx/warning.o \
src/util.o
src/verbosity.o
rgbasm: ${rgbasm_obj}
$Q${CXX} ${REALLDFLAGS} -o $@ ${rgbasm_obj} ${REALCXXFLAGS} src/version.cpp
@@ -233,10 +241,14 @@ coverage:
$Qenv ${MAKE} \
CXXFLAGS="-ggdb3 -Og --coverage -fno-omit-frame-pointer -fno-optimize-sibling-calls"
# Target used in development to format source code with clang-format.
format:
$Qclang-format -i $$(git ls-files '*.hpp' '*.cpp')
# Target used in development to check code with clang-tidy.
# Requires Bison-generated header files to exist.
tidy: src/asm/parser.hpp src/link/script.hpp
$Qclang-tidy -p . $$(find src -name '*.cpp')
$Qclang-tidy -p . $$(git ls-files '*.hpp' '*.cpp')
# Target used in development to remove unused `#include` headers.
iwyu:

122
README.md
View File

@@ -11,19 +11,19 @@ for the Game Boy and Game Boy Color. It consists of:
This is a fork of the original RGBDS which aims to make the programs more like
other UNIX tools.
This toolchain is maintained [on GitHub](https://github.com/gbdev/rgbds).
The documentation of this toolchain can be [viewed online](https://rgbds.gbdev.io/docs/).
The documentation of this toolchain can be [viewed online](https://rgbds.gbdev.io/docs/),
including its [basic usage and development history](https://rgbds.gbdev.io/docs/rgbds.7).
It is generated from the man pages found in this repository.
The source code of the website itself is on GitHub as well under the repository
[rgbds-www](https://github.com/gbdev/rgbds-www).
If you want to contribute or maintain RGBDS, or you have questions regarding the code, its
organization, etc. you can find the maintainers [on the gbdev community channels](https://gbdev.io/chat)
or via mail at `rgbds at gbdev dot io`.
If you want to contribute or maintain RGBDS, read [CONTRIBUTING.md](CONTRIBUTING.md).
If you have questions regarding the code, its organization, etc. you can find the maintainers
[on the GBDev community channels](https://gbdev.io/chat) or via mail at `rgbds at gbdev dot io`.
## 1. Installing RGBDS
## Installing RGBDS
The [installation procedure](https://rgbds.gbdev.io/install) is available
online for various platforms. [Building from source](https://rgbds.gbdev.io/install/source)
@@ -55,113 +55,3 @@ cmake --install build --prefix install_dir
```
(If you set a `SUFFIX`, it should include the `.exe` extension on Windows.)
## 2. RGBDS Folder Organization
The RGBDS source code file structure is as follows:
```
.
├── .github/
│ ├── scripts/
│ │ └── ...
│ └── workflows/
│ └── ...
├── contrib/
│ ├── zsh_compl/
│ │ └── ...
│ └── ...
├── include/
│ └── ...
├── man/
│ └── ...
├── src/
│ ├── asm/
│ │ └── ...
│ ├── extern/
│ │ └── ...
│ ├── fix/
│ │ └── ...
│ ├── gfx/
│ │ └── ...
│ ├── link/
│ │ └── ...
│ ├── CMakeLists.txt
│ └── ...
├── test/
│ ├── ...
│ └── run-tests.sh
├── .clang-format
├── CMakeLists.txt
├── compile_flags.txt
├── Dockerfile
├── Makefile
└── README.md
```
- `.github/` - files and scripts related to the integration of the RGBDS codebase with
GitHub.
* `scripts/` - scripts used by workflow files.
* `workflows/` - CI workflow description files.
- `contrib/` - scripts and other resources which may be useful to users and developers of
RGBDS.
* `zsh_compl` contains tab completion scripts for use with zsh. Put them somewhere in
your `fpath`, and they should auto-load.
* `bash_compl` contains tab completion scripts for use with bash. Run them with `source`
somewhere in your `.bashrc`, and they should load every time you open a shell.
- `include/` - header files for the respective source files in `src`.
- `man/` - manual pages.
- `src/` - source code of RGBDS.
* Note that the code unique to each RGBDS tool is stored in its respective subdirectory
(RGBASM's code is in `src/asm/`, for example). `src/extern/` contains code imported from
external sources.
- `test/` - testing framework used to verify that changes to the code don't break or
modify the behavior of RGBDS.
- `.clang-format` - code style for automated C++ formatting with
[`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
- `compile_flags.txt` - compiler flags for C++ static analysis with
[`clang-tidy`](https://clang.llvm.org/extra/clang-tidy/).
- `Dockerfile` - defines how to build RGBDS with Docker.
## 3. History
- 1996-10-01: Carsten Sørensen (a.k.a. SurfSmurf) releases
[xAsm](http://otakunozoku.com/RGBDSdocs/asm.htm),
[xLink](http://otakunozoku.com/RGBDSdocs/link.htm), and
[RGBFix](http://otakunozoku.com/RGBDSdocs/fix.htm),
a Game Boy SM83 (GBZ80) assembler/linker system for DOS/Win32.
- 1997-07-03: Sørensen releases [ASMotor](http://otakunozoku.com/RGBDSdocs/geninfo.htm),
packaging the three programs together and moving towards making them a
general-purpose target-independent system.
- 1999-08-01: Justin Lloyd (a.k.a. Otaku no Zoku) adapts ASMotor to re-focus
on SM83 assembly/machine code, and releases this version as
[RGBDS](http://otakunozoku.com/rednex-gameboy-development-system/).
- 2009-06-11: Vegard Nossum adapts the code to be more UNIX-like and releases
this version as [rgbds-linux](https://github.com/vegard/rgbds-linux).
- 2010-01-12: Anthony J. Bentley [forks](https://github.com/bentley) Nossum's
repository. The fork becomes the reference implementation of RGBDS.
- 2010-09-25: Sørensen continues development of
[ASMotor](https://github.com/asmotor/asmotor) to this day.
- 2015-01-18: stag019 begins implementing [RGBGFX](https://github.com/stag019/rgbgfx),
a PNG-to-Game Boy graphics converter, for eventual integration into RGBDS.
- 2016-09-05: RGBGFX is
[integrated](https://github.com/gbdev/rgbds/commit/c3c31138ddbd8680d4e67957e387f2816798a71b)
into Bentley's repository.
- 2017-02-23: Bentley's repository is moved to the [rednex](https://github.com/rednex)
organization.
- 2018-01-26: The codebase is [relicensed](https://github.com/gbdev/rgbds/issues/128)
under the MIT license.
- 2020-09-15: The repository is [moved](https://github.com/gbdev/rgbds/issues/567)
to the [gbdev](https://github.com/gbdev) organization.
- 2022-05-17: The [rgbds.gbdev.io](https://rgbds.gbdev.io) website for RGBDS
documentation and downloads is published.
## 4. Acknowledgements
RGBGFX generates palettes using algorithms found in the paper
["Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items"](https://arxiv.org/abs/1605.00558)
([GitHub](https://github.com/pagination-problem/pagination), MIT license),
by Aristide Grange, Imed Kacem, and Sébastien Martin.
RGBGFX's color palette was taken from [SameBoy](https://sameboy.github.io), with permission and help
by [LIJI](https://github.com/LIJI32).

View File

@@ -28,6 +28,7 @@ _rgbasm_completions() {
[V]="version:normal"
[W]="warning:warning"
[w]=":normal"
[B]="backtrace:unk"
[b]="binary-digits:unk"
[D]="define:unk"
[E]="export-all:normal"
@@ -61,7 +62,7 @@ _rgbasm_completions() {
parse_short_opt() {
# These options act like a long option (= takes up the entire word), but only use a single dash
# So, they need some special handling
if [[ "$1" = "-M"[GP] ]]; then
if [[ "$1" = "-M"[CGP] ]]; then
state=normal
optlen=${#1}
return;
@@ -146,7 +147,7 @@ _rgbasm_completions() {
# It is, try to complete one
mapfile -t COMPREPLY < <(compgen -W "${opts[*]%%:*}" -P '--' -- "${cur_word#--}")
return 0
elif [[ "$cur_word" = '-M'[GPQT] ]]; then
elif [[ "$cur_word" = '-M'[CGPQT] ]]; then
# These options act like long opts with no arguments, so return them and exactly them
COMPREPLY=( "$cur_word" )
return 0
@@ -183,6 +184,7 @@ _rgbasm_completions() {
empty-data-directive
empty-macro-arg
empty-strrpl
export-undefined
large-constant
macro-shift
nested-comment

View File

@@ -145,6 +145,7 @@ _rgbfix_completions() {
warning)
mapfile -t COMPREPLY < <(compgen -W "
mbc
obsolete
overwrite
sgb
truncation

View File

@@ -157,6 +157,7 @@ _rgbgfx_completions() {
warning)
mapfile -t COMPREPLY < <(compgen -W "
embedded
obsolete
trim-nonempty
all
everything

View File

@@ -12,6 +12,7 @@ _rgblink_completions() {
[W]="warning:warning"
[M]="no-sym-in-map:normal"
[d]="dmg:normal"
[B]="backtrace:unk"
[l]="linkerscript:glob-*"
[m]="map:glob-*.map"
[n]="sym:glob-*.sym"

View File

@@ -16,6 +16,7 @@ _rgbasm_warnings() {
'empty-data-directive:Warn on arg-less d[bwl] in ROM'
'empty-macro-arg:Warn on empty macro arg'
'empty-strrpl:Warn on calling STRRPL with empty pattern'
'export-undefined:Warn on EXPORT of an undefined symbol'
'large-constant:Warn on constants too large for a signed 32-bit int'
'macro-shift:Warn when shifting macro args part their limits'
'nested-comment:Warn on "/*" inside block comments'
@@ -39,10 +40,12 @@ local args=(
'(- : * options)'{-h,--help}'[Print help text and exit]'
'(-E --export-all)'{-E,--export-all}'[Export all symbols]'
'(-v --verbose)'{-v,--verbose}'[Print additional messages regarding progression]'
'(-v --verbose)'{-v,--verbose}'[Enable verbose output]'
-w'[Disable all warnings]'
'(-B --backtrace)'{-B,--backtrace}'+[Set backtrace depth or style]:param:'
'(-b --binary-digits)'{-b,--binary-digits}'+[Change chars for binary constants]:digit spec:'
--color'[Whether to use color in output]:color:(auto always never)'
'*'{-D,--define}'+[Define a string symbol]:name + value (default 1):'
'(-g --gfx-chars)'{-g,--gfx-chars}'+[Change chars for gfx constants]:chars spec:'
'(-I --include)'{-I,--include}'+[Add an include directory]:include path:_files -/'

View File

@@ -42,6 +42,7 @@ _rgbfix_warnings() {
'everything:Enable literally everything'
'mbc:Warn about issues with MBC specs'
'obsolete:Warn when using deprecated features'
'overwrite:Warn when overwriting non-zero bytes'
'sgb:Warn when SGB flag conflicts with old licensee code'
'truncation:Warn when values are truncated to fit'
@@ -62,6 +63,7 @@ local args=(
'(-f --fix-spec -v --validate)'{-v,--validate}'[Shorthand for -f lhg]'
-w'[Disable all warnings]'
--color'[Whether to use color in output]:color:(auto always never)'
'(-f --fix-spec -v --validate)'{-f,--fix-spec}'+[Fix or trash some header values]:fix spec:'
'(-i --game-id)'{-i,--game-id}'+[Set game ID string]:4-char game ID:'
'(-k --new-licensee)'{-k,--new-licensee}'+[Set new licensee string]:2-char licensee ID:'

View File

@@ -17,6 +17,7 @@ _rgbgfx_warnings() {
'everything:Enable literally everything'
'embedded:Warn when using embedded PLTE without "-c embedded"'
'obsolete:Warn when using deprecated features'
'trim-nonempty:Warn when "-x" trims nonempty tiles'
)
_describe warning warnings
@@ -44,6 +45,7 @@ local args=(
'(-a --attr-map -A --auto-attr-map)'{-a,--attr-map}'+[Generate a map of tile attributes (mirroring)]:attrmap file:_files'
'(-B --background-color)'{-B,--background-color}'+[Ignore tiles containing only specified color]:color:'
'(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:'
--color'[Whether to use color in output]:color:(auto always never)'
'(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:'
'(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths'
'(-i --input-tileset)'{-i,--input-tileset}'+[Use specific tiles]:tileset file:_files -g "*.2bpp"'

View File

@@ -28,6 +28,8 @@ local args=(
'(-w --wramx)'{-w,--wramx}'[Disable WRAM banking]'
'(-x --nopad)'{-x,--nopad}'[Disable padding the end of the final file]'
'(-B --backtrace)'{-B,--backtrace}'+[Set backtrace depth or style]:param:'
--color'[Whether to use color in output]:color:(auto always never)'
'(-l --linkerscript)'{-l,--linkerscript}"+[Use a linker script]:linker script:_files -g '*.link'"
'(-M --no-sym-in-map)'{-M,--no-sym-in-map}'[Do not output symbol names in map file]'
'(-m --map)'{-m,--map}"+[Produce a map file]:map file:_files -g '*.map'"

View File

@@ -4,39 +4,58 @@
#define RGBDS_ASM_ACTIONS_HPP
#include <optional>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#include "asm/output.hpp" // AssertionType
#include "asm/rpn.hpp" // RPNCommand
#include "linkdefs.hpp" // AssertionType, RPNCommand
#include "asm/rpn.hpp" // Expression
struct AlignmentSpec {
uint8_t alignment;
uint16_t alignOfs;
};
void act_If(int32_t condition);
void act_Elif(int32_t condition);
void act_Else();
void act_Endc();
AlignmentSpec act_Alignment(int32_t alignment, int32_t alignOfs);
void act_Assert(AssertionType type, Expression const &expr, std::string const &message);
void act_StaticAssert(AssertionType type, int32_t condition, std::string const &message);
std::optional<std::string> act_ReadFile(std::string const &name, uint32_t maxLen);
uint32_t act_StringToNum(std::vector<int32_t> const &str);
uint32_t act_CharToNum(std::string const &str);
uint32_t act_StringToNum(std::string const &str);
int32_t act_CharVal(std::string const &str);
int32_t act_CharVal(std::string const &str, int32_t negIdx);
uint8_t act_StringByte(std::string const &str, int32_t negIdx);
size_t act_StringLen(std::string const &str, bool printErrors);
std::string act_StringSlice(std::string const &str, uint32_t start, uint32_t stop);
std::string act_StringSub(std::string const &str, uint32_t pos, uint32_t len);
std::string
act_StringSlice(std::string const &str, int32_t negStart, std::optional<int32_t> negStop);
std::string act_StringSub(std::string const &str, int32_t negPos, std::optional<uint32_t> optLen);
size_t act_CharLen(std::string const &str);
std::string act_StringChar(std::string const &str, uint32_t idx);
std::string act_CharSub(std::string const &str, uint32_t pos);
std::string act_StringChar(std::string const &str, int32_t negIdx);
std::string act_CharSub(std::string const &str, int32_t negPos);
int32_t act_CharCmp(std::string_view str1, std::string_view str2);
uint32_t act_AdjustNegativeIndex(int32_t idx, size_t len, char const *functionName);
uint32_t act_AdjustNegativePos(int32_t pos, size_t len, char const *functionName);
std::string act_StringReplace(std::string_view str, std::string const &old, std::string const &rep);
std::string act_StringFormat(
std::string const &spec, std::vector<std::variant<uint32_t, std::string>> const &args
);
std::string act_SectionName(std::string const &symName);
void act_CompoundAssignment(std::string const &symName, RPNCommand op, int32_t constValue);
void act_FailAssert(AssertionType type);
void act_FailAssertMsg(AssertionType type, std::string const &message);
#endif // RGBDS_ASM_ACTIONS_HPP

View File

@@ -4,6 +4,7 @@
#define RGBDS_ASM_CHARMAP_HPP
#include <optional>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <string_view>

View File

@@ -7,19 +7,7 @@
#include <stdint.h>
#include <string>
enum FormatState {
FORMAT_SIGN, // expects '+' or ' ' (optional)
FORMAT_EXACT, // expects '#' (optional)
FORMAT_ALIGN, // expects '-' (optional)
FORMAT_WIDTH, // expects '0'-'9', max 255 (optional) (leading '0' indicates pad)
FORMAT_FRAC, // got '.', expects '0'-'9', max 255 (optional)
FORMAT_PREC, // got 'q', expects '0'-'9', range 1-31 (optional)
FORMAT_DONE, // got [duXxbofs] (required)
FORMAT_INVALID, // got unexpected character
};
class FormatSpec {
FormatState state;
int sign;
bool exact;
bool alignLeft;
@@ -30,15 +18,13 @@ class FormatSpec {
bool hasPrec;
size_t precision;
int type;
bool valid;
bool parsed;
public:
bool isEmpty() const { return !state; }
bool isValid() const { return valid || state == FORMAT_DONE; }
bool isFinished() const { return state >= FORMAT_DONE; }
bool isValid() const { return !!type; }
bool isParsed() const { return parsed; }
void useCharacter(int c);
void finishCharacters();
size_t parseSpec(char const *spec);
void appendString(std::string &str, std::string const &value) const;
void appendNumber(std::string &str, uint32_t value) const;

View File

@@ -24,6 +24,7 @@ struct FileStackNode {
std::string // NODE_FILE, NODE_MACRO
>
data;
bool isQuiet; // Whether to omit this node from error reporting
std::shared_ptr<FileStackNode> parent; // Pointer to parent node, for error reporting
// Line at which the parent context was exited
@@ -40,16 +41,21 @@ struct FileStackNode {
std::string &name() { return std::get<std::string>(data); }
std::string const &name() const { return std::get<std::string>(data); }
FileStackNode(FileStackNodeType type_, std::variant<std::vector<uint32_t>, std::string> data_)
: type(type_), data(data_) {}
FileStackNode(
FileStackNodeType type_,
std::variant<std::vector<uint32_t>, std::string> data_,
bool isQuiet_
)
: type(type_), data(data_), isQuiet(isQuiet_) {}
std::string const &dump(uint32_t curLineNo) const;
std::string reptChain() const;
void printBacktrace(uint32_t curLineNo) const;
};
struct MacroArgs;
bool fstk_DumpCurrent();
void fstk_VerboseOutputConfig();
void fstk_TraceCurrent();
std::shared_ptr<FileStackNode> fstk_GetFileStack();
std::shared_ptr<std::string> fstk_GetUniqueIDStr();
MacroArgs *fstk_GetCurrentMacroArgs();
@@ -61,16 +67,19 @@ bool fstk_FileError(std::string const &path, char const *functionName);
bool fstk_FailedOnMissingInclude();
bool yywrap();
bool fstk_RunInclude(std::string const &path);
void fstk_RunMacro(std::string const &macroName, std::shared_ptr<MacroArgs> macroArgs);
void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span);
bool fstk_RunInclude(std::string const &path, bool isQuiet);
void fstk_RunMacro(
std::string const &macroName, std::shared_ptr<MacroArgs> macroArgs, bool isQuiet
);
void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span, bool isQuiet);
void fstk_RunFor(
std::string const &symName,
int32_t start,
int32_t stop,
int32_t step,
int32_t reptLineNo,
ContentSpan const &span
ContentSpan const &span,
bool isQuiet
);
bool fstk_Break();

View File

@@ -6,6 +6,7 @@
#include <deque>
#include <memory>
#include <optional>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <variant>
@@ -133,7 +134,7 @@ void lexer_ReachELSEBlock();
void lexer_CheckRecursionDepth();
uint32_t lexer_GetLineNo();
void lexer_DumpStringExpansions();
void lexer_TraceStringExpansions();
struct Capture {
uint32_t lineNo;

View File

@@ -3,6 +3,7 @@
#ifndef RGBDS_ASM_MAIN_HPP
#define RGBDS_ASM_MAIN_HPP
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <string>
@@ -14,16 +15,16 @@ enum MissingInclude {
};
struct Options {
bool exportAll = false; // -E
uint8_t fixPrecision = 16; // -Q
size_t maxRecursionDepth = 64; // -r
char binDigits[2] = {'0', '1'}; // -b
char gfxDigits[4] = {'0', '1', '2', '3'}; // -g
bool verbose = false; // -v
FILE *dependFile = nullptr; // -M
std::string targetFileName; // -MQ, -MT
std::optional<std::string> targetFileName{}; // -MQ, -MT
MissingInclude missingIncludeState = INC_ERROR; // -MC, -MG
bool generatePhonyDeps = false; // -MP
std::string objectFileName; // -o
std::optional<std::string> objectFileName{}; // -o
uint8_t padByte = 0; // -p
uint64_t maxErrors = 0; // -X
@@ -35,18 +36,11 @@ struct Options {
void printDep(std::string const &depName) {
if (dependFile) {
fprintf(dependFile, "%s: %s\n", targetFileName.c_str(), depName.c_str());
fprintf(dependFile, "%s: %s\n", targetFileName->c_str(), depName.c_str());
}
}
};
extern Options options;
#define verbosePrint(...) \
do { \
if (options.verbose) { \
fprintf(stderr, __VA_ARGS__); \
} \
} while (0)
#endif // RGBDS_ASM_MAIN_HPP

View File

@@ -6,17 +6,18 @@
#include <memory>
#include <stdint.h>
#include <string>
#include <unordered_map>
#include <vector>
#include "linkdefs.hpp"
struct Expression;
struct FileStackNode;
struct Symbol;
enum StateFeature { STATE_EQU, STATE_VAR, STATE_EQUS, STATE_CHAR, STATE_MACRO, NB_STATE_FEATURES };
void out_RegisterNode(std::shared_ptr<FileStackNode> node);
void out_RegisterSymbol(Symbol &sym);
void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32_t pcShift);
void out_CreateAssert(
AssertionType type, Expression const &expr, std::string const &message, uint32_t ofs

View File

@@ -12,15 +12,25 @@
struct Symbol;
struct RPNValue {
RPNCommand command; // The RPN_* command ID
std::variant<std::monostate, uint8_t, uint32_t, std::string> data; // Data after the ID, if any
RPNValue(RPNCommand cmd);
RPNValue(RPNCommand cmd, uint8_t val);
RPNValue(RPNCommand cmd, uint32_t val);
RPNValue(RPNCommand cmd, std::string const &name);
void appendEncoded(std::vector<uint8_t> &buffer) const;
};
struct Expression {
std::variant<
int32_t, // If the expression's value is known, it's here
std::string // Why the expression is not known, if it isn't
>
data = 0;
bool isSymbol = false; // Whether the expression represents a symbol suitable for const diffing
std::vector<uint8_t> rpn{}; // Bytes serializing the RPN expression
uint32_t rpnPatchSize = 0; // Size the expression will take in the object file
std::vector<RPNValue> rpn{}; // Values to be serialized into the RPN expression
bool isKnown() const { return std::holds_alternative<int32_t>(data); }
int32_t value() const { return std::get<int32_t>(data); }
@@ -40,16 +50,13 @@ struct Expression {
void makeUnaryOp(RPNCommand op, Expression &&src);
void makeBinaryOp(RPNCommand op, Expression &&src1, Expression const &src2);
bool makeCheckHRAM();
void makeCheckRST();
void makeCheckBitIndex(uint8_t mask);
void addCheckHRAM();
void addCheckRST();
void addCheckBitIndex(uint8_t mask);
void checkNBit(uint8_t n) const;
private:
void clear();
uint8_t *reserveSpace(uint32_t size);
uint8_t *reserveSpace(uint32_t size, uint32_t patchSize);
void encode(std::vector<uint8_t> &buffer) const;
};
bool checkNBit(int32_t v, uint8_t n, char const *name);

View File

@@ -6,9 +6,9 @@
#include <deque>
#include <memory>
#include <optional>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <unordered_map>
#include <vector>
#include "linkdefs.hpp"

View File

@@ -5,9 +5,7 @@
#include <memory>
#include <stdint.h>
#include <string.h>
#include <string>
#include <string_view>
#include <time.h>
#include <utility>
#include <variant>
@@ -30,8 +28,9 @@ bool sym_IsPC(Symbol const *sym); // Forward declaration for `getSection`
struct Symbol {
std::string name;
SymbolType type;
bool isExported; // Whether the symbol is to be exported
bool isBuiltin; // Whether the symbol is a built-in
bool isBuiltin;
bool isExported; // Not relevant for SYM_MACRO or SYM_EQUS
bool isQuiet; // Only relevant for SYM_MACRO
Section *section;
std::shared_ptr<FileStackNode> src; // Where the symbol was defined
uint32_t fileLine; // Line where the symbol was defined
@@ -71,7 +70,6 @@ struct Symbol {
void sym_ForEach(void (*callback)(Symbol &));
void sym_SetExportAll(bool set);
Symbol *sym_AddLocalLabel(std::string const &symName);
Symbol *sym_AddLabel(std::string const &symName);
Symbol *sym_AddAnonLabel();
@@ -89,7 +87,9 @@ Symbol *sym_FindScopedSymbol(std::string const &symName);
// Find a scoped symbol by name; do not return `@` or `_NARG` when they have no value
Symbol *sym_FindScopedValidSymbol(std::string const &symName);
Symbol const *sym_GetPC();
Symbol *sym_AddMacro(std::string const &symName, int32_t defLineNo, ContentSpan const &span);
Symbol *sym_AddMacro(
std::string const &symName, int32_t defLineNo, ContentSpan const &span, bool isQuiet
);
Symbol *sym_Ref(std::string const &symName);
Symbol *sym_AddString(std::string const &symName, std::shared_ptr<std::string> value);
Symbol *sym_RedefString(std::string const &symName, std::shared_ptr<std::string> value);

View File

@@ -23,6 +23,7 @@ enum WarningID {
WARNING_EMPTY_DATA_DIRECTIVE, // `db`, `dw` or `dl` directive without data in ROM
WARNING_EMPTY_MACRO_ARG, // Empty macro argument
WARNING_EMPTY_STRRPL, // Empty second argument in `STRRPL`
WARNING_EXPORT_UNDEFINED, // `EXPORT` of an undefined symbol
WARNING_LARGE_CONSTANT, // Constants too large
WARNING_MACRO_SHIFT, // `SHIFT` past available arguments in macro
WARNING_NESTED_COMMENT, // Comment-start delimiter in a block comment
@@ -74,11 +75,10 @@ void fatal(char const *fmt, ...);
[[gnu::format(printf, 1, 2)]]
void error(char const *fmt, ...);
// 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
// once.
void error(std::function<void()> callback);
// Used for errors that handle their own backtrace output. 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 once.
void errorNoTrace(std::function<void()> callback);
void requireZeroErrors();

89
include/backtrace.hpp Normal file
View File

@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_BACKTRACE_HPP
#define RGBDS_BACKTRACE_HPP
#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <vector>
#include "style.hpp"
#define TRACE_SEPARATOR "<-"
#define NODE_SEPARATOR "::"
#define REPT_NODE_PREFIX "REPT~"
struct Tracing {
uint64_t depth = 0;
bool collapse = false;
bool loud = false;
};
extern Tracing tracing;
bool trace_ParseTraceDepth(char const *arg);
template<typename NodeT, typename NameFnT, typename LineNoFnT>
void trace_PrintBacktrace(std::vector<NodeT> const &stack, NameFnT getName, LineNoFnT getLineNo) {
size_t n = stack.size();
if (n == 0) {
return; // LCOV_EXCL_LINE
}
auto printLocation = [&](size_t i) {
NodeT const &item = stack[n - i - 1];
style_Reset(stderr);
if (!tracing.collapse) {
fputs(" ", stderr); // Just three spaces; the fourth will be printed next
}
fprintf(stderr, " %s ", i == 0 ? "at" : TRACE_SEPARATOR);
style_Set(stderr, STYLE_CYAN, true);
fputs(getName(item), stderr);
style_Set(stderr, STYLE_CYAN, false);
fprintf(stderr, "(%" PRIu32 ")", getLineNo(item));
if (!tracing.collapse) {
putc('\n', stderr);
}
};
if (tracing.collapse) {
fputs(" ", stderr); // Just three spaces; the fourth will be handled by the loop
}
if (tracing.depth == 0 || static_cast<size_t>(tracing.depth) >= n) {
for (size_t i = 0; i < n; ++i) {
printLocation(i);
}
} else {
size_t last = tracing.depth / 2;
size_t first = tracing.depth - last;
size_t skipped = n - tracing.depth;
for (size_t i = 0; i < first; ++i) {
printLocation(i);
}
style_Reset(stderr);
if (tracing.collapse) {
fputs(" " TRACE_SEPARATOR, stderr);
} else {
fputs(" ", stderr); // Just three spaces; the fourth will be printed next
}
fprintf(stderr, " ...%zu more%s", skipped, last ? "..." : "");
if (!tracing.collapse) {
putc('\n', stderr);
}
for (size_t i = n - last; i < n; ++i) {
printLocation(i);
}
}
if (tracing.collapse) {
putc('\n', stderr);
}
style_Reset(stderr);
}
#endif // RGBDS_BACKTRACE_HPP

21
include/cli.hpp Normal file
View File

@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_CLI_HPP
#define RGBDS_CLI_HPP
#include <stdarg.h>
#include <string>
#include "extern/getopt.hpp" // option
#include "usage.hpp"
void cli_ParseArgs(
int argc,
char *argv[],
char const *shortOpts,
option const *longOpts,
void (*parseArg)(int, char *),
Usage usage
);
#endif // RGBDS_CLI_HPP

View File

@@ -9,8 +9,8 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <utility>
#include <vector>
#include "helpers.hpp"
@@ -30,35 +30,35 @@ struct WarningState {
std::pair<WarningState, std::optional<uint32_t>> getInitialWarningState(std::string &flag);
template<typename L>
template<typename LevelEnumT>
struct WarningFlag {
char const *name;
L level;
LevelEnumT level;
};
enum WarningBehavior { DISABLED, ENABLED, ERROR };
template<typename W>
template<typename WarningEnumT>
struct ParamWarning {
W firstID;
W lastID;
WarningEnumT firstID;
WarningEnumT lastID;
uint8_t defaultLevel;
};
template<typename W>
template<typename WarningEnumT>
struct DiagnosticsState {
WarningState flagStates[W::NB_WARNINGS];
WarningState metaStates[W::NB_WARNINGS];
WarningState flagStates[WarningEnumT::NB_WARNINGS];
WarningState metaStates[WarningEnumT::NB_WARNINGS];
bool warningsEnabled = true;
bool warningsAreErrors = false;
};
template<typename L, typename W>
template<typename LevelEnumT, typename WarningEnumT>
struct Diagnostics {
std::vector<WarningFlag<L>> metaWarnings;
std::vector<WarningFlag<L>> warningFlags;
std::vector<ParamWarning<W>> paramWarnings;
DiagnosticsState<W> state;
std::vector<WarningFlag<LevelEnumT>> metaWarnings;
std::vector<WarningFlag<LevelEnumT>> warningFlags;
std::vector<ParamWarning<WarningEnumT>> paramWarnings;
DiagnosticsState<WarningEnumT> state;
uint64_t nbErrors;
void incrementErrors() {
@@ -67,12 +67,12 @@ struct Diagnostics {
}
}
WarningBehavior getWarningBehavior(W id) const;
std::string processWarningFlag(char const *flag);
WarningBehavior getWarningBehavior(WarningEnumT id) const;
void processWarningFlag(char const *flag);
};
template<typename L, typename W>
WarningBehavior Diagnostics<L, W>::getWarningBehavior(W id) const {
template<typename LevelEnumT, typename WarningEnumT>
WarningBehavior Diagnostics<LevelEnumT, WarningEnumT>::getWarningBehavior(WarningEnumT id) const {
// Check if warnings are globally disabled
if (!state.warningsEnabled) {
return WarningBehavior::DISABLED;
@@ -112,7 +112,7 @@ WarningBehavior Diagnostics<L, W>::getWarningBehavior(W id) const {
}
// If no meta flag is specified, check the default state of this warning flag
if (warningFlags[id].level == L::LEVEL_DEFAULT) { // enabled by default
if (warningFlags[id].level == LevelEnumT::LEVEL_DEFAULT) { // enabled by default
return enabledBehavior;
}
@@ -120,19 +120,19 @@ WarningBehavior Diagnostics<L, W>::getWarningBehavior(W id) const {
return WarningBehavior::DISABLED;
}
template<typename L, typename W>
std::string Diagnostics<L, W>::processWarningFlag(char const *flag) {
template<typename LevelEnumT, typename WarningEnumT>
void Diagnostics<LevelEnumT, WarningEnumT>::processWarningFlag(char const *flag) {
std::string rootFlag = flag;
// Check for `-Werror` or `-Wno-error` to return early
if (rootFlag == "error") {
// `-Werror` promotes warnings to errors
state.warningsAreErrors = true;
return rootFlag;
return;
} else if (rootFlag == "no-error") {
// `-Wno-error` disables promotion of warnings to errors
state.warningsAreErrors = false;
return rootFlag;
return;
}
auto [flagState, param] = getInitialWarningState(rootFlag);
@@ -140,8 +140,8 @@ std::string Diagnostics<L, W>::processWarningFlag(char const *flag) {
// Try to match the flag against a parametric warning
// If there was an equals sign, it will have set `param`; if not, `param` will be 0,
// which applies to all levels
for (ParamWarning<W> const &paramWarning : paramWarnings) {
W baseID = paramWarning.firstID;
for (ParamWarning<WarningEnumT> const &paramWarning : paramWarnings) {
WarningEnumT baseID = paramWarning.firstID;
uint8_t maxParam = paramWarning.lastID - baseID + 1;
assume(paramWarning.defaultLevel <= maxParam);
@@ -174,39 +174,38 @@ std::string Diagnostics<L, W>::processWarningFlag(char const *flag) {
warning.state = WARNING_DISABLED;
}
}
return rootFlag;
return;
}
if (param.has_value()) {
warnx("Unknown warning flag parameter \"%s=%" PRIu32 "\"", rootFlag.c_str(), *param);
return rootFlag;
return;
}
// Try to match against a "meta" warning
for (WarningFlag<L> const &metaWarning : metaWarnings) {
for (WarningFlag<LevelEnumT> const &metaWarning : metaWarnings) {
if (rootFlag != metaWarning.name) {
continue;
}
// Set each of the warning flags that meets this level
for (W id : EnumSeq(W::NB_WARNINGS)) {
for (WarningEnumT id : EnumSeq(WarningEnumT::NB_WARNINGS)) {
if (metaWarning.level >= warningFlags[id].level) {
state.metaStates[id].update(flagState);
}
}
return rootFlag;
return;
}
// Try to match against a "normal" flag
for (W id : EnumSeq(W::NB_PLAIN_WARNINGS)) {
for (WarningEnumT id : EnumSeq(WarningEnumT::NB_PLAIN_WARNINGS)) {
if (rootFlag == warningFlags[id].name) {
state.flagStates[id].update(flagState);
return rootFlag;
return;
}
}
warnx("Unknown warning flag \"%s\"", rootFlag.c_str());
return rootFlag;
}
#endif // RGBDS_DIAGNOSTICS_HPP

View File

@@ -12,7 +12,7 @@ static constexpr int optional_argument = 2;
// clang-format on
extern char *musl_optarg;
extern int musl_optind, musl_opterr, musl_optopt, musl_optreset;
extern int musl_optind, musl_optopt;
struct option {
char const *name;
@@ -21,8 +21,6 @@ struct option {
int val;
};
int musl_getopt_long_only(
int argc, char **argv, char const *optstring, option const *longopts, int *idx
);
int musl_getopt_long_only(int argc, char **argv, char const *optstring, option const *longopts);
#endif // RGBDS_EXTERN_GETOPT_HPP

View File

@@ -8,7 +8,6 @@
#include <ios>
#include <iostream>
#include <streambuf>
#include <string.h>
#include <string>
#include <variant>

8
include/fix/fix.hpp Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_FIX_FIX_HPP
#define RGBDS_FIX_FIX_HPP
bool fix_ProcessFile(char const *name, char const *outputName);
#endif // RGBDS_FIX_FIX_HPP

51
include/fix/main.hpp Normal file
View File

@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_FIX_MAIN_HPP
#define RGBDS_FIX_MAIN_HPP
#include <optional>
#include <stdint.h>
#include <string>
#include "fix/mbc.hpp" // UNSPECIFIED, MbcType
// clang-format off: vertically align values
static constexpr uint8_t FIX_LOGO = 1 << 7;
static constexpr uint8_t TRASH_LOGO = 1 << 6;
static constexpr uint8_t FIX_HEADER_SUM = 1 << 5;
static constexpr uint8_t TRASH_HEADER_SUM = 1 << 4;
static constexpr uint8_t FIX_GLOBAL_SUM = 1 << 3;
static constexpr uint8_t TRASH_GLOBAL_SUM = 1 << 2;
// clang-format on
enum Model { DMG, BOTH, CGB };
struct Options {
uint8_t fixSpec = 0; // -f, -v
Model model = DMG; // -C, -c
bool japanese = true; // -j
uint16_t oldLicensee = UNSPECIFIED; // -l
uint16_t romVersion = UNSPECIFIED; // -n
uint16_t padValue = UNSPECIFIED; // -p
uint16_t ramSize = UNSPECIFIED; // -r
bool sgb = false; // -s
std::optional<std::string> gameID; // -i
uint8_t gameIDLen;
std::optional<std::string> newLicensee; // -k
uint8_t newLicenseeLen;
std::optional<std::string> logoFilename; // -L
uint8_t logo[48] = {};
MbcType cartridgeType = MBC_NONE; // -m
uint8_t tpp1Rev[2];
std::optional<std::string> title; // -t
uint8_t titleLen;
};
extern Options options;
#endif // RGBDS_FIX_MAIN_HPP

View File

@@ -3,6 +3,8 @@
#ifndef RGBDS_FIX_WARNING_HPP
#define RGBDS_FIX_WARNING_HPP
#include <stdint.h>
#include "diagnostics.hpp"
enum WarningLevel {
@@ -13,6 +15,7 @@ enum WarningLevel {
enum WarningID {
WARNING_MBC, // Issues with MBC specs
WARNING_OBSOLETE, // Obsolete/deprecated things
WARNING_OVERWRITE, // Overwriting non-zero bytes
WARNING_SGB, // SGB flag conflicts with old licensee code
WARNING_TRUNCATION, // Truncating values to fit

23
include/gfx/flip.hpp Normal file
View File

@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_GFX_FLIP_HPP
#define RGBDS_GFX_FLIP_HPP
#include <array>
#include <stdint.h>
// Flipping tends to happen fairly often, so take a bite out of dcache to speed it up
static std::array<uint16_t, 256> flipTable = ([]() constexpr {
std::array<uint16_t, 256> table{};
for (uint16_t i = 0; i < table.size(); ++i) {
// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
uint16_t byte = i;
byte = (byte & 0b0000'1111) << 4 | (byte & 0b1111'0000) >> 4;
byte = (byte & 0b0011'0011) << 2 | (byte & 0b1100'1100) >> 2;
byte = (byte & 0b0101'0101) << 1 | (byte & 0b1010'1010) >> 1;
table[i] = byte;
}
return table;
})();
#endif // RGBDS_GFX_FLIP_HPP

View File

@@ -7,10 +7,9 @@
#include <optional>
#include <stdint.h>
#include <string>
#include <utility>
#include <vector>
#include "helpers.hpp"
#include "helpers.hpp" // assume
#include "gfx/rgba.hpp"
@@ -20,7 +19,6 @@ struct Options {
bool allowMirroringX = false; // -X, -m
bool allowMirroringY = false; // -Y, -m
bool columnMajor = false; // -Z
uint8_t verbosity = 0; // -v
std::string attrmap{}; // -a, -A
std::optional<Rgba> bgColor{}; // -B
@@ -56,18 +54,6 @@ struct Options {
std::string input{}; // positional arg
// clang-format off: vertically align values
static constexpr uint8_t VERB_NONE = 0; // Normal, no extra output
static constexpr uint8_t VERB_CFG = 1; // Print configuration after parsing options
static constexpr uint8_t VERB_LOG_ACT = 2; // Log actions before doing them
static constexpr uint8_t VERB_INTERM = 3; // Print some intermediate results
static constexpr uint8_t VERB_DEBUG = 4; // Internals are logged
static constexpr uint8_t VERB_TRACE = 5; // Step-by-step algorithm details
static constexpr uint8_t VERB_VVVVVV = 6; // What, can't I have a little fun?
// clang-format on
[[gnu::format(printf, 3, 4)]]
void verbosePrint(uint8_t level, char const *fmt, ...) const;
mutable bool hasTransparentPixels = false;
uint8_t maxOpaqueColors() const { return nbColorsPerPal - hasTransparentPixels; }
@@ -82,35 +68,4 @@ struct Options {
extern Options options;
struct Palette {
// An array of 4 GBC-native (RGB555) colors
std::array<uint16_t, 4> colors{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
void addColor(uint16_t color);
uint8_t indexOf(uint16_t color) const;
uint16_t &operator[](size_t index) { return colors[index]; }
uint16_t const &operator[](size_t index) const { return colors[index]; }
decltype(colors)::iterator begin();
decltype(colors)::iterator end();
decltype(colors)::const_iterator begin() const;
decltype(colors)::const_iterator end() const;
uint8_t size() const;
};
// Flipping tends to happen fairly often, so take a bite out of dcache to speed it up
static std::array<uint16_t, 256> flipTable = ([]() constexpr {
std::array<uint16_t, 256> table{};
for (uint16_t i = 0; i < table.size(); ++i) {
// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
uint16_t byte = i;
byte = (byte & 0b0000'1111) << 4 | (byte & 0b1111'0000) >> 4;
byte = (byte & 0b0011'0011) << 2 | (byte & 0b1100'1100) >> 2;
byte = (byte & 0b0101'0101) << 1 | (byte & 0b1010'1010) >> 1;
table[i] = byte;
}
return table;
})();
#endif // RGBDS_GFX_MAIN_HPP

View File

@@ -4,13 +4,13 @@
#define RGBDS_GFX_PAL_PACKING_HPP
#include <stddef.h>
#include <tuple>
#include <utility>
#include <vector>
struct Palette;
class ColorSet;
// Returns which palette each color set maps to, and how many palettes are necessary
std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet> const &colorSets);
std::pair<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet> const &colorSets);
#endif // RGBDS_GFX_PAL_PACKING_HPP

View File

@@ -5,6 +5,7 @@
#include <array>
#include <optional>
#include <stddef.h>
#include <vector>
#include "gfx/rgba.hpp"

View File

@@ -3,9 +3,12 @@
#ifndef RGBDS_GFX_PAL_SPEC_HPP
#define RGBDS_GFX_PAL_SPEC_HPP
#include <stdint.h>
void parseInlinePalSpec(char const * const rawArg);
void parseExternalPalSpec(char const *arg);
void parseDmgPalSpec(char const * const rawArg);
void parseDmgPalSpec(uint8_t palSpecDmg);
void parseBackgroundPalSpec(char const *arg);

27
include/gfx/palette.hpp Normal file
View File

@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_GFX_PALETTE_HPP
#define RGBDS_GFX_PALETTE_HPP
#include <array>
#include <stddef.h>
#include <stdint.h>
struct Palette {
// An array of 4 GBC-native (RGB555) colors
std::array<uint16_t, 4> colors{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
void addColor(uint16_t color);
uint8_t indexOf(uint16_t color) const;
uint16_t &operator[](size_t index) { return colors[index]; }
uint16_t const &operator[](size_t index) const { return colors[index]; }
decltype(colors)::iterator begin();
decltype(colors)::iterator end();
decltype(colors)::const_iterator begin() const;
decltype(colors)::const_iterator end() const;
uint8_t size() const;
};
#endif // RGBDS_GFX_PALETTE_HPP

View File

@@ -3,8 +3,8 @@
#ifndef RGBDS_GFX_PNG_HPP
#define RGBDS_GFX_PNG_HPP
#include <fstream>
#include <stdint.h>
#include <streambuf>
#include <vector>
#include "gfx/rgba.hpp"

View File

@@ -18,10 +18,7 @@ struct Rgba {
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
static constexpr Rgba fromCGBColor(uint16_t color) {
constexpr auto _5to8 = [](uint8_t channel) -> uint8_t {
channel &= 0b11111; // For caller's convenience
return channel << 3 | channel >> 2;
};
constexpr auto _5to8 = [](uint8_t c) -> uint8_t { return ((c & 0b11111) * 255 + 15) / 31; };
return {
_5to8(color),
_5to8(color >> 5),

View File

@@ -13,6 +13,7 @@ enum WarningLevel {
enum WarningID {
WARNING_EMBEDDED, // Using an embedded PNG palette without '-c embedded'
WARNING_OBSOLETE, // Obsolete/deprecated things
WARNING_TRIM_NONEMPTY, // '-x' trims nonempty tiles
NB_PLAIN_WARNINGS,

View File

@@ -97,18 +97,19 @@ static inline int clz(unsigned int x) {
// For lack of <ranges>, this adds some more brevity
#define RANGE(s) std::begin(s), std::end(s)
#define RRANGE(s) std::rbegin(s), std::rend(s)
// MSVC does not inline `strlen()` or `.length()` of a constant string
template<int N>
static constexpr int literal_strlen(char const (&)[N]) {
return N - 1;
template<int SizeOfString>
static constexpr int literal_strlen(char const (&)[SizeOfString]) {
return SizeOfString - 1; // Don't count the ending '\0'
}
// For ad-hoc RAII in place of a `defer` statement or cross-platform `__attribute__((cleanup))`
template<typename T>
template<typename DeferredFnT>
struct Defer {
T deferred;
Defer(T func) : deferred(func) {}
DeferredFnT deferred;
Defer(DeferredFnT func) : deferred(func) {}
~Defer() { deferred(); }
};

View File

@@ -3,46 +3,119 @@
#ifndef RGBDS_ITERTOOLS_HPP
#define RGBDS_ITERTOOLS_HPP
#include <deque>
#include <optional>
#include <stddef.h>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <utility>
template<typename T>
class EnumSeq {
T _start;
T _stop;
// A wrapper around iterables to reverse their iteration order; used in `for`-each loops.
template<typename IterableT>
struct ReversedIterable {
IterableT &_iterable;
};
class Iterator {
T _value;
template<typename IterableT>
auto begin(ReversedIterable<IterableT> r) {
return std::rbegin(r._iterable);
}
template<typename IterableT>
auto end(ReversedIterable<IterableT> r) {
return std::rend(r._iterable);
}
template<typename IterableT>
ReversedIterable<IterableT> reversed(IterableT &&_iterable) {
return {_iterable};
}
// A map from `std::string` keys to `ItemT` items, iterable in the order the items were inserted.
template<typename ItemT>
class InsertionOrderedMap {
std::deque<ItemT> list;
std::unordered_map<std::string, size_t> map; // Indexes into `list`
public:
explicit Iterator(T value) : _value(value) {}
size_t size() const { return list.size(); }
bool empty() const { return list.empty(); }
bool contains(std::string const &name) const { return map.find(name) != map.end(); }
ItemT &operator[](size_t i) { return list[i]; }
typename decltype(list)::iterator begin() { return list.begin(); }
typename decltype(list)::iterator end() { return list.end(); }
typename decltype(list)::const_iterator begin() const { return list.begin(); }
typename decltype(list)::const_iterator end() const { return list.end(); }
ItemT &add(std::string const &name) {
map[name] = list.size();
return list.emplace_back();
}
ItemT &add(std::string const &name, ItemT &&value) {
map[name] = list.size();
list.emplace_back(std::move(value));
return list.back();
}
ItemT &addAnonymous() {
// Add the new item to the list, but do not update the map
return list.emplace_back();
}
std::optional<size_t> findIndex(std::string const &name) const {
if (auto search = map.find(name); search != map.end()) {
return search->second;
}
return std::nullopt;
}
};
// An iterable of `enum` values in the half-open range [start, stop).
template<typename EnumT>
class EnumSeq {
EnumT _start;
EnumT _stop;
class Iterator {
EnumT _value;
public:
explicit Iterator(EnumT value) : _value(value) {}
Iterator &operator++() {
_value = static_cast<T>(_value + 1);
_value = static_cast<EnumT>(_value + 1);
return *this;
}
T operator*() const { return _value; }
EnumT operator*() const { return _value; }
bool operator==(Iterator const &rhs) const { return _value == rhs._value; }
};
public:
explicit EnumSeq(T stop) : _start(static_cast<T>(0)), _stop(stop) {}
explicit EnumSeq(T start, T stop) : _start(start), _stop(stop) {}
explicit EnumSeq(EnumT stop) : _start(static_cast<EnumT>(0)), _stop(stop) {}
explicit EnumSeq(EnumT start, EnumT stop) : _start(start), _stop(stop) {}
Iterator begin() { return Iterator(_start); }
Iterator end() { return Iterator(_stop); }
};
// Only needed inside `ZipContainer` below.
// This is not a fully generic implementation; its current use cases only require for-loop behavior.
// We also assume that all iterators have the same length.
template<typename... Ts>
template<typename... IteratorTs>
class ZipIterator {
std::tuple<Ts...> _iters;
std::tuple<IteratorTs...> _iters;
public:
explicit ZipIterator(std::tuple<Ts...> &&iters) : _iters(iters) {}
explicit ZipIterator(std::tuple<IteratorTs...> &&iters) : _iters(iters) {}
ZipIterator &operator++() {
std::apply([](auto &&...it) { (++it, ...); }, _iters);
@@ -60,12 +133,14 @@ public:
}
};
template<typename... Ts>
// Only needed inside `zip` below.
template<typename... IterableTs>
class ZipContainer {
std::tuple<Ts...> _containers;
std::tuple<IterableTs...> _containers;
public:
explicit ZipContainer(Ts &&...containers) : _containers(std::forward<Ts>(containers)...) {}
explicit ZipContainer(IterableTs &&...containers)
: _containers(std::forward<IterableTs>(containers)...) {}
auto begin() {
return ZipIterator(std::apply(
@@ -88,15 +163,19 @@ public:
}
};
// Only needed inside `zip` below.
// Take ownership of objects and rvalue refs passed to us, but not lvalue refs
template<typename T>
using Holder = std::
conditional_t<std::is_lvalue_reference_v<T>, T, std::remove_cv_t<std::remove_reference_t<T>>>;
template<typename IterableT>
using ZipHolder = std::conditional_t<
std::is_lvalue_reference_v<IterableT>,
IterableT,
std::remove_cv_t<std::remove_reference_t<IterableT>>>;
// Iterates over N containers at once, yielding tuples of N items at a time.
// Does the same number of iterations as the first container's iterator!
template<typename... Ts>
static constexpr auto zip(Ts &&...cs) {
return ZipContainer<Holder<Ts>...>(std::forward<Ts>(cs)...);
template<typename... IterableTs>
static constexpr auto zip(IterableTs &&...containers) {
return ZipContainer<ZipHolder<IterableTs>...>(std::forward<IterableTs>(containers)...);
}
#endif // RGBDS_ITERTOOLS_HPP

38
include/link/fstack.hpp Normal file
View File

@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_LINK_FSTACK_HPP
#define RGBDS_LINK_FSTACK_HPP
#include <stdint.h>
#include <stdio.h>
#include <string>
#include <variant>
#include <vector>
#include "linkdefs.hpp"
struct FileStackNode {
FileStackNodeType type;
std::variant<
std::monostate, // Default constructed; `.type` and `.data` must be set manually
std::vector<uint32_t>, // NODE_REPT
std::string // NODE_FILE, NODE_MACRO
>
data;
bool isQuiet; // Whether to omit this node from error reporting
FileStackNode *parent;
// Line at which the parent context was exited; meaningless for the root level
uint32_t lineNo;
// REPT iteration counts since last named node, in reverse depth order
std::vector<uint32_t> &iters() { return std::get<std::vector<uint32_t>>(data); }
std::vector<uint32_t> const &iters() const { return std::get<std::vector<uint32_t>>(data); }
// File name for files, file::macro name for macros
std::string &name() { return std::get<std::string>(data); }
std::string const &name() const { return std::get<std::string>(data); }
void printBacktrace(uint32_t curLineNo) const;
};
#endif // RGBDS_LINK_FSTACK_HPP

View File

@@ -3,15 +3,13 @@
#ifndef RGBDS_LINK_LEXER_HPP
#define RGBDS_LINK_LEXER_HPP
#include <stdarg.h>
#include <string>
[[gnu::format(printf, 1, 2)]]
void lexer_Error(char const *fmt, ...);
void lexer_TraceCurrent();
void lexer_IncludeFile(std::string &&path);
void lexer_IncLineNo();
bool lexer_Init(char const *linkerScriptName);
bool lexer_Init(std::string const &linkerScriptName);
#endif // RGBDS_LINK_LEXER_HPP

View File

@@ -3,21 +3,17 @@
#ifndef RGBDS_LINK_MAIN_HPP
#define RGBDS_LINK_MAIN_HPP
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <string>
#include <variant>
#include <vector>
#include "linkdefs.hpp"
struct Options {
bool isDmgMode; // -d
char const *mapFileName; // -m
std::optional<std::string> mapFileName; // -m
bool noSymInMap; // -M
char const *symFileName; // -n
char const *overlayFileName; // -O
char const *outputFileName; // -o
std::optional<std::string> symFileName; // -n
std::optional<std::string> overlayFileName; // -O
std::optional<std::string> outputFileName; // -o
uint8_t padValue; // -p
bool hasPadValue = false;
// Setting these three to 0 disables the functionality
@@ -25,41 +21,10 @@ struct Options {
uint16_t scrambleWRAMX;
uint16_t scrambleSRAM;
bool is32kMode; // -t
bool beVerbose; // -v
bool isWRAM0Mode; // -w
bool disablePadding; // -x
};
extern Options options;
#define verbosePrint(...) \
do { \
if (options.beVerbose) { \
fprintf(stderr, __VA_ARGS__); \
} \
} while (0)
struct FileStackNode {
FileStackNodeType type;
std::variant<
std::monostate, // Default constructed; `.type` and `.data` must be set manually
std::vector<uint32_t>, // NODE_REPT
std::string // NODE_FILE, NODE_MACRO
>
data;
FileStackNode *parent;
// Line at which the parent context was exited; meaningless for the root level
uint32_t lineNo;
// REPT iteration counts since last named node, in reverse depth order
std::vector<uint32_t> &iters() { return std::get<std::vector<uint32_t>>(data); }
std::vector<uint32_t> const &iters() const { return std::get<std::vector<uint32_t>>(data); }
// File name for files, file::macro name for macros
std::string &name() { return std::get<std::string>(data); }
std::string const &name() const { return std::get<std::string>(data); }
std::string const &dump(uint32_t curLineNo) const;
};
#endif // RGBDS_LINK_MAIN_HPP

View File

@@ -3,10 +3,13 @@
#ifndef RGBDS_LINK_OBJECT_HPP
#define RGBDS_LINK_OBJECT_HPP
#include <stddef.h>
#include <string>
// Read an object (.o) file, and add its info to the data structures.
void obj_ReadFile(char const *fileName, unsigned int fileID);
void obj_ReadFile(std::string const &filePath, size_t fileID);
// Sets up object file reading
void obj_Setup(unsigned int nbFiles);
void obj_Setup(size_t nbFiles);
#endif // RGBDS_LINK_OBJECT_HPP

View File

@@ -3,9 +3,6 @@
#ifndef RGBDS_LINK_SECTION_HPP
#define RGBDS_LINK_SECTION_HPP
// GUIDELINE: external code MUST NOT BE AWARE of the data structure used!
#include <deque>
#include <memory>
#include <stdint.h>
#include <string>
@@ -13,8 +10,6 @@
#include "linkdefs.hpp"
#include "link/main.hpp"
struct FileStackNode;
struct Section;
struct Symbol;
@@ -53,7 +48,40 @@ struct Section {
// Extra info computed during linking
std::vector<Symbol> *fileSymbols;
std::vector<Symbol *> symbols;
std::unique_ptr<Section> nextu; // The next "component" of this unionized sect
std::unique_ptr<Section> nextPiece; // The next fragment or union "piece" of this section
private:
// Template class for both const and non-const iterators over the "pieces" of this section
template<typename SectionT>
class PiecesIterable {
SectionT *_firstPiece;
class Iterator {
SectionT *_piece;
public:
explicit Iterator(SectionT *piece) : _piece(piece) {}
Iterator &operator++() {
_piece = _piece->nextPiece.get();
return *this;
}
SectionT &operator*() const { return *_piece; }
bool operator==(Iterator const &rhs) const { return _piece == rhs._piece; }
};
public:
explicit PiecesIterable(SectionT *firstPiece) : _firstPiece(firstPiece) {}
Iterator begin() { return Iterator(_firstPiece); }
Iterator end() { return Iterator(nullptr); }
};
public:
PiecesIterable<Section> pieces() { return PiecesIterable(this); }
PiecesIterable<Section const> pieces() const { return PiecesIterable(this); }
};
// Execute a callback for each section currently registered.

View File

@@ -33,8 +33,8 @@ struct Symbol {
>
data;
Label &label() { return std::get<Label>(data); }
Label const &label() const { return std::get<Label>(data); }
void linkToSection(Section &section);
void fixSectionOffset();
};
void sym_ForEach(void (*callback)(Symbol &));
@@ -44,6 +44,6 @@ void sym_AddSymbol(Symbol &symbol);
// Finds a symbol in all the defined symbols.
Symbol *sym_GetSymbol(std::string const &name);
void sym_DumpLocalAliasedSymbols(std::string const &name);
void sym_TraceLocalAliasedSymbols(std::string const &name);
#endif // RGBDS_LINK_SYMBOL_HPP

View File

@@ -8,9 +8,12 @@
#include "diagnostics.hpp"
#define warningAt(where, ...) warning(where.src, where.lineNo, __VA_ARGS__)
#define errorAt(where, ...) error(where.src, where.lineNo, __VA_ARGS__)
#define fatalAt(where, ...) fatal(where.src, where.lineNo, __VA_ARGS__)
#define warningAt(where, ...) warning((where).src, (where).lineNo, __VA_ARGS__)
#define errorAt(where, ...) error((where).src, (where).lineNo, __VA_ARGS__)
#define fatalAt(where, ...) fatal((where).src, (where).lineNo, __VA_ARGS__)
#define fatalTwoAt(where1, where2, ...) \
fatalTwo(*(where1).src, (where1).lineNo, *(where2).src, (where2).lineNo, __VA_ARGS__)
enum WarningLevel {
LEVEL_DEFAULT, // Warnings that are enabled by default
@@ -24,11 +27,14 @@ enum WarningID {
WARNING_OBSOLETE, // Obsolete/deprecated things
WARNING_SHIFT, // Undefined `SHIFT` behavior
WARNING_SHIFT_AMOUNT, // Strange `SHIFT` amount
WARNING_TRUNCATION, // Implicit truncation loses some bits
NB_PLAIN_WARNINGS,
NB_WARNINGS = NB_PLAIN_WARNINGS,
// Implicit truncation loses some bits
WARNING_TRUNCATION_1 = NB_PLAIN_WARNINGS,
WARNING_TRUNCATION_2,
NB_WARNINGS,
};
extern Diagnostics<WarningLevel, WarningID> warnings;
@@ -47,15 +53,23 @@ void error(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...);
[[gnu::format(printf, 1, 2)]]
void error(char const *fmt, ...);
[[gnu::format(printf, 1, 2)]]
void errorNoDump(char const *fmt, ...);
void scriptError(char const *name, uint32_t lineNo, char const *fmt, va_list args);
void scriptError(char const *fmt, ...);
[[gnu::format(printf, 3, 4), noreturn]]
void fatal(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...);
[[gnu::format(printf, 1, 2), noreturn]]
void fatal(char const *fmt, ...);
[[gnu::format(printf, 5, 6), noreturn]]
void fatalTwo(
FileStackNode const &src1,
uint32_t lineNo1,
FileStackNode const &src2,
uint32_t lineNo2,
char const *fmt,
...
);
void requireZeroErrors();
#endif // RGBDS_LINK_WARNING_HPP

View File

@@ -9,7 +9,7 @@
#include "helpers.hpp" // assume
#define RGBDS_OBJECT_VERSION_STRING "RGB9"
#define RGBDS_OBJECT_REV 12U
#define RGBDS_OBJECT_REV 13U
enum AssertionType { ASSERT_WARN, ASSERT_ERROR, ASSERT_FATAL };
@@ -78,12 +78,18 @@ enum SectionType {
SECTTYPE_INVALID
};
static constexpr uint8_t SECTTYPE_TYPE_MASK = 0b111;
static constexpr uint8_t SECTTYPE_UNION_BIT = 7;
static constexpr uint8_t SECTTYPE_FRAGMENT_BIT = 6;
enum FileStackNodeType {
NODE_REPT,
NODE_FILE,
NODE_MACRO,
};
static constexpr uint8_t FSTACKNODE_QUIET_BIT = 7;
// Nont-`const` members may be patched in RGBLINK depending on CLI flags
extern struct SectionTypeInfo {
std::string const name;
@@ -95,18 +101,18 @@ extern struct SectionTypeInfo {
// Tells whether a section has data in its object file definition,
// depending on type.
static inline bool sect_HasData(SectionType type) {
static inline bool sectTypeHasData(SectionType type) {
assume(type != SECTTYPE_INVALID);
return type == SECTTYPE_ROM0 || type == SECTTYPE_ROMX;
}
// Returns a memory region's end address (last byte), e.g. 0x7FFF
static inline uint16_t endaddr(SectionType type) {
static inline uint16_t sectTypeEndAddr(SectionType type) {
return sectionTypeInfo[type].startAddr + sectionTypeInfo[type].size - 1;
}
// Returns a memory region's number of banks, or 1 for regions without banking
static inline uint32_t nbbanks(SectionType type) {
static inline uint32_t sectTypeBanks(SectionType type) {
return sectionTypeInfo[type].lastBank - sectionTypeInfo[type].firstBank + 1;
}

View File

@@ -8,8 +8,17 @@
int32_t op_divide(int32_t dividend, int32_t divisor);
int32_t op_modulo(int32_t dividend, int32_t divisor);
int32_t op_exponent(int32_t base, uint32_t power);
int32_t op_shift_left(int32_t value, int32_t amount);
int32_t op_shift_right(int32_t value, int32_t amount);
int32_t op_shift_right_unsigned(int32_t value, int32_t amount);
int32_t op_neg(int32_t value);
int32_t op_high(int32_t value);
int32_t op_low(int32_t value);
int32_t op_bitwidth(int32_t value);
int32_t op_tzcount(int32_t value);
#endif // RGBDS_OP_MATH_HPP

View File

@@ -28,6 +28,7 @@
#define STDERR_FILENO 2
#define ssize_t int
#define SSIZE_MAX INT_MAX
#define isatty _isatty
#else
#include <fcntl.h> // IWYU pragma: export
#include <limits.h> // IWYU pragma: export
@@ -54,9 +55,18 @@
#define setmode(fd, mode) (0)
#endif
// MingGW and Cygwin need POSIX functions which are not standard C explicitly enabled
// MingGW and Cygwin need POSIX functions which are not standard C explicitly enabled,
#if defined(__MINGW32__) || defined(__CYGWIN__)
#define _POSIX_C_SOURCE 200809L
#endif
// gcc and clang have their own `musttail` attributes for tail recursion
#if defined(__clang__) && __has_cpp_attribute(clang::musttail)
#define MUSTTAIL [[clang::musttail]]
#elif defined(__GNUC__) && __has_cpp_attribute(gnu::musttail)
#define MUSTTAIL [[gnu::musttail]]
#else
#define MUSTTAIL
#endif
#endif // RGBDS_PLATFORM_HPP

42
include/style.hpp Normal file
View File

@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_STYLE_HPP
#define RGBDS_STYLE_HPP
#include <stdio.h>
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__CYGWIN__)
#define STYLE_ANSI 0
#else
#define STYLE_ANSI 1
#endif
enum StyleColor {
#if STYLE_ANSI
// Values analogous to ANSI foreground and background SGR colors
STYLE_BLACK,
STYLE_RED,
STYLE_GREEN,
STYLE_YELLOW,
STYLE_BLUE,
STYLE_MAGENTA,
STYLE_CYAN,
STYLE_GRAY,
#else
// Values analogous to `FOREGROUND_*` constants from `windows.h`
STYLE_BLACK,
STYLE_BLUE, // bit 0
STYLE_GREEN, // bit 1
STYLE_CYAN, // STYLE_BLUE | STYLE_GREEN
STYLE_RED, // bit 2
STYLE_MAGENTA, // STYLE_BLUE | STYLE_RED
STYLE_YELLOW, // STYLE_GREEN | STYLE_RED
STYLE_GRAY, // STYLE_BLUE | STYLE_GREEN | STYLE_RED
#endif
};
bool style_Parse(char const *arg);
void style_Set(FILE *file, StyleColor color, bool bold);
void style_Reset(FILE *file);
#endif // RGBDS_STYLE_HPP

View File

@@ -4,12 +4,14 @@
#define RGBDS_USAGE_HPP
#include <stdarg.h>
#include <string>
#include <utility>
#include <vector>
class Usage {
char const *usage;
public:
Usage(char const *usage_) : usage(usage_) {}
struct Usage {
std::string name;
std::vector<std::string> flags;
std::vector<std::pair<std::vector<std::string>, std::vector<std::string>>> options;
[[noreturn]]
void printAndExit(int code) const;

View File

@@ -3,23 +3,69 @@
#ifndef RGBDS_UTIL_HPP
#define RGBDS_UTIL_HPP
#include <algorithm>
#include <numeric>
#include <optional>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <unordered_map>
#include "helpers.hpp"
enum NumberBase {
BASE_AUTO = 0,
BASE_2 = 2,
BASE_8 = 8,
BASE_10 = 10,
BASE_16 = 16,
};
// Locale-independent character class functions
bool isNewline(int c);
bool isBlankSpace(int c);
bool isWhitespace(int c);
bool isPrintable(int c);
bool isUpper(int c);
bool isLower(int c);
bool isLetter(int c);
bool isDigit(int c);
bool isBinDigit(int c);
bool isOctDigit(int c);
bool isHexDigit(int c);
bool isAlphanumeric(int c);
// Locale-independent character transform functions
char toLower(char c);
char toUpper(char c);
bool startsIdentifier(int c);
bool continuesIdentifier(int c);
uint8_t parseHexDigit(int c);
std::optional<uint64_t> parseNumber(char const *&str, NumberBase base = BASE_AUTO);
std::optional<uint64_t> parseWholeNumber(char const *str, NumberBase base = BASE_AUTO);
char const *printChar(int c);
struct Uppercase {
size_t operator()(std::string const &str) const;
bool operator()(std::string const &str1, std::string const &str2) const;
// FNV-1a hash of an uppercased string
constexpr size_t operator()(std::string const &str) const {
return std::accumulate(RANGE(str), 0x811C9DC5, [](size_t hash, char c) {
return (hash ^ toUpper(c)) * 16777619;
});
}
// Compare two strings without case-sensitivity (by converting to uppercase)
constexpr bool operator()(std::string const &str1, std::string const &str2) const {
return std::equal(RANGE(str1), RANGE(str2), [](char c1, char c2) {
return toUpper(c1) == toUpper(c2);
});
}
};
template<typename T>
using UpperMap = std::unordered_map<std::string, T, Uppercase, Uppercase>;
// An unordered map from case-insensitive `std::string` keys to `ItemT` items
template<typename ItemT>
using UpperMap = std::unordered_map<std::string, ItemT, Uppercase, Uppercase>;
#endif // RGBDS_UTIL_HPP

35
include/verbosity.hpp Normal file
View File

@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_VERBOSITY_HPP
#define RGBDS_VERBOSITY_HPP
#include <stdio.h>
#include "style.hpp"
// This macro does not evaluate its arguments unless the condition is true.
#define verbosePrint(level, ...) \
do { \
if (checkVerbosity(level)) { \
style_Set(stderr, STYLE_MAGENTA, false); \
fprintf(stderr, __VA_ARGS__); \
style_Reset(stderr); \
} \
} while (0)
enum Verbosity {
VERB_NONE, // 0. Default, no extra output
VERB_CONFIG, // 1. Basic configuration, after parsing CLI options
VERB_NOTICE, // 2. Before significant actions
VERB_INFO, // 3. Some intermediate action results
VERB_DEBUG, // 4. Internals useful for debugging
VERB_TRACE, // 5. Step-by-step algorithm details
VERB_VVVVVV, // 6. What, can't I have a little fun?
};
void incrementVerbosity();
bool checkVerbosity(Verbosity level);
void printVVVVVVerbosity();
#endif // RGBDS_VERBOSITY_HPP

View File

@@ -3,9 +3,10 @@
#ifndef RGBDS_VERSION_HPP
#define RGBDS_VERSION_HPP
#define PACKAGE_VERSION_MAJOR 0
#define PACKAGE_VERSION_MINOR 9
#define PACKAGE_VERSION_PATCH 4
#define PACKAGE_VERSION_MAJOR 1
#define PACKAGE_VERSION_MINOR 0
#define PACKAGE_VERSION_PATCH 0
#define PACKAGE_VERSION_RC 2
char const *get_package_version_string();

View File

@@ -1,13 +1,13 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt GBZ80 7
.Os
.Sh NAME
.Nm gbz80
.Nd CPU opcode reference
.Nd Game Boy CPU instruction reference
.Sh DESCRIPTION
This is the list of opcodes supported by
This is the list of instructions supported by
.Xr rgbasm 1 ,
including a short description, the number of bytes needed to encode them and the number of CPU cycles at 1MHz (or 2MHz in GBC double speed mode) needed to complete them.
.Pp
@@ -657,7 +657,7 @@ Set if result is 0.
.It Sy H
0
.It Sy C
Set or reset depending on the operation.
Set or unaffected depending on the operation.
.El
.Ss DEC r8
Decrement the value in register

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBASM-OLD 5
.Os
.Sh NAME
@@ -129,12 +129,23 @@ Deprecated in 0.5.0, removed in 0.6.0.
.Pp
Instead, use
.Ql 3.141592653 .
.Ss __DATE__ and __TIME__
Deprecated in 1.0.0.
.Pp
Instead, use
.Ql __ISO_8601_LOCAL__ .
.Ss Treating multi-character strings as numbers
Deprecated in 0.9.0.
Deprecated in 0.9.0, removed in 1.0.0.
.Pp
Instead, use a multi-value
.Ic CHARMAP ,
or explicitly combine the values of individual characters.
.Ss Treating strings as numbers
Deprecated in 1.0.0.
.Pp
Instead, use character constants or the
.Ic CHARVAL
function.
.Ss rgbgfx -f/--fix and -F/--fix-and-save
Removed in 0.6.0.
.Pp
@@ -209,6 +220,31 @@ typed in column 1.
Instead, use
.Ql \&;
comments.
.Ss STRIN, STRRIN, STRSUB, and CHARSUB
Deprecated in 1.0.0.
.Pp
These functions used 1-based indexing of string characters, which was inconsistent with the 0-based indexing used more often in programming.
.Pp
Instead of
.Ic STRIN ,
use
.Ic STRFIND ;
instead of
.Ic STRRIN ,
use
.Ic STRRFIND ;
instead of
.Ic STRSUB ,
use
.Ic STRSLICE ;
and instead of
.Ic CHARSUB ,
use
.Ic STRCHAR .
.Pp
Note that
.Ic STRSLICE
takes a start and end index instead of a start index and a length.
.Ss PRINTT, PRINTI, PRINTV, and PRINTF
Deprecated in 0.5.0, removed in 0.6.0.
.Pp
@@ -247,7 +283,7 @@ and
.Ic DATA ,
use
.Ic ROMX ;
instead of
and instead of
.Ic BSS ,
use
.Ic WRAM0 .
@@ -272,12 +308,12 @@ or
and
.Ql LD A, [HL-] ) .
.Ss LDIO
Deprecated in 0.9.0.
Deprecated in 0.9.0, removed in 1.0.0.
.Pp
Instead, use
.Ql LDH .
.Ss LD [C], A and LD A, [C]
Deprecated in 0.9.0.
Deprecated in 0.9.0, removed in 1.0.0.
.Pp
Instead, use
.Ql LDH [C], A
@@ -292,7 +328,7 @@ were also deprecated in 0.9.0, but were
.Em undeprecated
in 0.9.1.
.Ss LDH [n8], A and LDH A, [n8]
Deprecated in 0.9.0.
Deprecated in 0.9.0, removed in 1.0.0.
.Pp
.Ql LDH
used to treat "addresses" from
@@ -329,19 +365,24 @@ Deprecated in 0.6.0, removed in 0.8.0.
Instead, use
.Fl I
or
.Fl -include .
.Ss rgbgfx -h
.Fl \-include .
.Ss rgbfix -O/--overwrite
Deprecated in 1.0.0.
.Pp
Instead, use
.Dl -Wno-overwrite .
.Ss rgbgfx -h/--horizontal
Removed in 0.6.0.
.Pp
Instead, use
.Fl Z
or
.Fl -columns .
.Fl \-columns .
.Ss rgbgfx --output-*
Deprecated in 0.7.0, removed in 0.8.0.
.Pp
Instead, use
.Fl -auto-* .
.Fl \-auto-* .
.Sh CHANGED
These are breaking changes that did not alter syntax, and so could not practically be deprecated.
.Ss Trigonometry function units
@@ -420,6 +461,24 @@ Previously we had
.Pp
Instead, now we have
.Ql p ** q ** r == p ** (q ** r) .
.Ss 8-bit and 5-bit color conversion
Changed in 1.0.0.
.Pp
RGBGFX takes 8-bit RGB colors as its PNG input, and outputs 5-bit GBC colors.
Its
.Ql -r/--reverse
mode does the opposite 5-bit to 8-bit conversion.
Instead of the previous inaccurate conversions, we now do accurate rounding to the nearest equivalent.
.Pp
Previously to convert an 8-bit color channel to 5-bit, we truncated it as
.Ql c >> 3 ;
and to reverse a 5-bit color channel to 8-bit, we extended it as
.Ql (c << 3) | (c >> 2) .
.Pp
Instead, now we round 8-bit to 5-bit as
.Ql (c * 31 + 127) / 255 ,
and round 5-bit to 8-bit as
.Ql (c * 255 + 15) / 31 .
.Sh BUGS
These are misfeatures that may have been possible by mistake.
They do not get deprecated, just fixed.
@@ -450,6 +509,13 @@ Instead, use
.Ql Label:
and
.Ql Label:: .
.Ss Extra underscores in integer constants
Fixed in 1.0.0.
.Pp
Underscores, the optional digit separators in integer constants, used to allow more than one in sequence, or trailing without digits on either side.
Now only one underscore is allowed between two digits, or between the base prefix and a digit, or between a digit and the
.Ql q
fixed-point precision suffix.
.Ss ADD r16 with implicit first HL operand
Fixed in 0.5.0.
.Pp

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBASM 1
.Os
.Sh NAME
@@ -9,7 +9,9 @@
.Sh SYNOPSIS
.Nm
.Op Fl EhVvw
.Op Fl B Ar param
.Op Fl b Ar chars
.Op Fl \-color Ar when
.Op Fl D Ar name Ns Op = Ns Ar value
.Op Fl g Ar chars
.Op Fl I Ar path
@@ -34,14 +36,14 @@ The
program creates an RGB object file from an assembly source file.
The object file format is documented in
.Xr rgbds 5 .
.Pp
The input
.Ar asmfile
can be a path to a file, or
.Cm \-
to read from standard input.
.Pp
Note that options can be abbreviated as long as the abbreviation is unambiguous:
.Sh ARGUMENTS
.Nm
accepts the usual short and long options, such as
.Fl V
and
.Fl -version .
Options later in the command line override those set earlier, except for when duplicate options are considered an error.
Options can be abbreviated as long as the abbreviation is unambiguous:
.Fl \-verb
is
.Fl \-verbose ,
@@ -49,8 +51,76 @@ but
.Fl \-ver
is invalid because it could also be
.Fl \-version .
The arguments are as follows:
.Pp
Unless otherwise noted, passing
.Ql -
(a single dash) as a file name makes
.Nm
use standard input (for input files) or standard output (for output files).
To suppress this behavior, and open a file in the current directory actually called
.Ql - ,
pass
.Ql ./-
instead.
Using standard input or output for more than one file in a single command may produce unexpected results.
.Pp
.Nm
accepts decimal, hexadecimal, octal, and binary for numeric option arguments.
Decimal numbers are written as usual; hexadecimal numbers must be prefixed with either
.Ql $
or
.Ql 0x ;
octal numbers must be prefixed with either
.Ql &
or
.Ql 0o ;
and binary numbers must be prefixed with either
.Ql %
or
.Ql 0b .
(The prefixes
.Ql $
and
.Ql &
will likely need escaping or quoting to avoid being interpreted by the shell.)
Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive.
For example, all of these are equivalent:
.Ql 42 ,
.Ql 042 ,
.Ql 0x2A ,
.Ql 0X2A ,
.Ql 0x2a ,
.Ql &52 ,
.Ql 0o52 ,
.Ql 0O052 ,
.Ql 0b00101010 ,
.Ql 0B101010 .
.Pp
The following options are accepted:
.Bl -tag -width Ds
.It Fl B Ar param , Fl \-backtrace Ar param
Configures how location backtraces are printed if warnings or errors occur.
This flag may be specified multiple times with different parameters that combine meaningfully.
If
.Ar param
is a positive number, it specifies the maximum backtrace depth, abbreviating deeper ones.
Other valid parameter values are the following:
.Bl -tag -width Ds
.It Cm 0
Do not limit the maximum backtrace depth; this is the default.
.It Cm all
Force all locations to be printed, even "quiet" ones (see
.Dq Excluding locations from backtraces
in
.Xr rgbasm 5
for details).
.It Cm no-all
Do not print "quieted" locations in backtraces; this is the default.
.It Cm collapse
Print all locations on one line.
.It Cm no-collapse
Print one location per line; this is the default.
.El
.It Fl b Ar chars , Fl \-binary-digits Ar chars
Allow two characters to be used for binary constants in addition to the default
.Sq 0
@@ -65,6 +135,18 @@ letters,
.Sq # ,
or
.Sq @ .
.It Fl \-color Ar when
Specify when to highlight warning and error messages with color:
.Ql always ,
.Ql never ,
or
.Ql auto .
.Ql auto
determines whether to use colors based on the
.Ql Lk https://no-color.org/ NO_COLOR
or
.Ql Lk https://force-color.org/ FORCE_COLOR
environment variables, or whether the output is to a TTY.
.It Fl D Ar name Ns Oo = Ns Ar value Oc , Fl \-define Ar name Ns Oo = Ns Ar value Oc
Add a string symbol to the compiled source code.
This is equivalent to
@@ -233,6 +315,24 @@ below).
Print the version of the program and exit.
.It Fl v , Fl \-verbose
Be verbose.
The verbosity level is increased by one each time the flag is specified, with each level including the previous:
.Bl -enum -compact
.It
Print the
.Nm
configuration before taking actions.
.It
Print a notice before significant actions.
.It
Print some of the actions' intermediate results.
.It
Print some internal debug information.
.It
Print detailed internal information.
.El
The verbosity level does not go past 6.
.Pp
Note that verbose output is only intended to be consumed by humans, and may change without notice between RGBDS releases; relying on those for scripts is not advised.
.It Fl W Ar warning , Fl \-warning Ar warning
Set warning flag
.Ar warning .
@@ -251,6 +351,23 @@ disables this behavior.
The default is 100 if
.Nm
is printing errors to a terminal, and 0 otherwise.
.It @ Ns Ar at_file
Read more options and arguments from a file, as if its contents were given on the command line.
Arguments are separated by whitespace or newlines.
Lines starting with a hash sign
.Pq Ql #
are considered comments and ignored.
.Pp
No shell processing is performed, such as wildcard or variable expansion.
There is no support for escaping or quoting whitespace to be included in arguments.
The standard
.Ql --
to stop option processing also disables at-file processing.
Note that while
.Ql --
can be used
.Em inside
an at-file, it only disables option processing within that at-file, and processing continues in the parent scope.
.El
.Sh DIAGNOSTICS
Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the assembling process.
@@ -289,9 +406,9 @@ Enables literally every warning.
.Pp
The following warnings are actual warning flags; with each description, the corresponding warning flag is included.
Note that each of these flags also has a negation (for example,
.Fl Wcharmap-redef
.Fl Wobsolete
enables the warning that
.Fl Wno-charmap-redef
.Fl Wno-obsolete
disables; and
.Fl Wall
enables every warning that
@@ -329,6 +446,15 @@ This warning is enabled by
.Fl Wall .
.It Fl Wdiv
Warn when dividing the smallest negative integer (-2**31) by -1, which yields itself due to integer overflow.
.It Fl Wempty-data-directive
Warn when
.Ic DB ,
.Ic DW ,
or
.Ic DL
is used without an argument in a ROM section.
This warning is enabled by
.Fl Wall .
.It Fl Wempty-macro-arg
Warn when a macro argument is empty.
This warning is enabled by
@@ -339,10 +465,12 @@ Warn when
is called with an empty string as its second argument (the substring to replace).
This warning is enabled by
.Fl Wall .
.It Fl Wlarge-constant
Warn when a constant too large to fit in a signed 32-bit integer is encountered.
.It Fl Wexport-undefined
Warn when exporting an undefined symbol.
This warning is enabled by
.Fl Wall .
.It Fl Wno-large-constant
Warn when a constant too large to fit in a signed 32-bit integer is encountered.
.It Fl Wmacro-shift
Warn when shifting macro arguments past their limits.
This warning is enabled by
@@ -375,10 +503,10 @@ or
.Fl Wno-purge
disables this warning.
.Fl Wpurge=1
or just
.Fl Wpurge
warns when purging any exported symbol (regardless of type).
.Fl Wpurge=2
or just
.Fl Wpurge
also warns when purging any label (even if not exported).
.It Fl Wshift
Warn when shifting right a negative value.
@@ -394,10 +522,10 @@ or
.Fl Wno-truncation
disables this warning.
.Fl Wtruncation=1
warns when an N-bit value is 2**N or greater, or less than -2**N.
.Fl Wtruncation=2
or just
.Fl Wtruncation
warns when an N-bit value is 2**N or greater, or less than -2**N.
.Fl Wtruncation=2
also warns when an N-bit value is less than -2**(N-1), which will not fit in two's complement encoding.
.It Fl Wunmapped-char=
Warn when a character goes through charmap conversion but has no defined mapping.
@@ -461,7 +589,7 @@ Writing the final assembler state to a file:
Or to multiple files:
.Dl $ rgbasm -s equ,var:numbers.dump.asm -s equs:strings.dump.asm foo.asm
.Sh BUGS
Please report bugs on
Please report bugs or mistakes in this documentation on
.Lk https://github.com/gbdev/rgbds/issues GitHub .
.Sh SEE ALSO
.Xr rgbasm 5 ,

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBASM 5
.Os
.Sh NAME
@@ -43,7 +43,9 @@ Labels tie a name to a specific location within a section (see
below).
.Pp
Instructions are assembled into Game Boy opcodes.
Multiple instructions on one line can be separated by double colons
Multiple instructions on one line, as well as data directives (see
.Sx Defining constant data in ROM
below), can be separated by double colons
.Ql :: .
.Pp
The available instructions are documented in
@@ -194,12 +196,19 @@ If specified, pads right-aligned numbers with zeros instead of spaces.
If specified, pads the value to this width, right-aligned with spaces by default.
.It Ql <frac> Ta May be
.Ql \&.
followed by one or more
followed by zero or more
.Ql 0
\[en]
.Ql 9 .
If specified, prints this many fractional digits of a fixed-point number.
Defaults to 5 digits, maximum 255 digits.
(A
.Ql \&.
followed by zero
.Ql 0
\[en]
.Ql 9
prints zero fractional digits.)
.It Ql <prec> Ta May be
.Ql q
followed by one or more
@@ -577,6 +586,19 @@ is equivalent to
or to
.Ql STRCAT("str", \&"ing") .
.Pp
You can use the
.Sq ===
and
.Sq !==
operators to compare two strings.
.Ql \&"str" === \&"ing"
is equivalent to
.Ql STRCMP("str", \&"ing") == 0 ,
and
.Ql \&"str" !== \&"ing"
is equivalent to
.Ql STRCMP("str", \&"ing") != 0 .
.Pp
The following functions operate on string expressions, and return strings themselves.
.Bl -column "STRSLICE(str, start, stop)"
.It Sy Name Ta Sy Operation
@@ -615,7 +637,7 @@ The following functions operate on string expressions, but return integers.
.It Fn CHARLEN str Ta Returns the number of charmap entries in Ar str No with the current charmap.
.It Fn CHARCMP str1 str2 Ta Compares Ar str1 No and Ar str2 No according to their charmap entry values with the current charmap. Returns -1 if Ar str1 No is lower than Ar str2 Ns , 1 if Ar str1 No is greater than Ar str2 Ns , or 0 if they match.
.It Fn CHARSIZE char Ta Returns how many values are in the charmap entry for Ar char No with the current charmap.
.It Fn CHARVAL char idx Ta Returns the value at Ar idx No of the charmap entry for Ar char .
.It Fn CHARVAL char idx Ta Returns the value at Ar idx No of the charmap entry for Ar char . If Ar idx No is not specified, Ar char No must have a single value, which is returned.
.El
.Pp
Note that indexes count starting from 0 at the beginning, or from -1 at the end.
@@ -625,28 +647,16 @@ the charmap entries of a string are counted by
.Ql CHARLEN ;
and the values of a charmap entry are counted by
.Ql CHARSIZE .
.Pp
The following legacy functions are similar to other functions that operate on string expressions, but for historical reasons, they count starting from
.Em position 1 ,
not from index 0!
(Position -1 still counts from the end.)
.Bl -column "STRSUB(str, pos, len)"
.It Sy Name Ta Sy Operation
.It Fn STRSUB str pos len Ta Returns a substring of Ar str No starting at Ar pos No and Ar len No characters long. If Ar len No is not specified, the substring continues to the end of Ar str No .
.It Fn STRIN str sub Ta Returns the first position of Ar sub No in Ar str Ns , or 0 if it's not present.
.It Fn STRRIN str sub Ta Returns the last position of Ar sub No in Ar str Ns , or 0 if it's not present.
.It Fn CHARSUB str pos Ta Returns the substring of Ar str No for the charmap entry at Ar pos No with the current charmap . Pq Ar pos No counts charmap entries, not characters.
.El
.Ss Character maps
When writing text strings that are meant to be displayed on the Game Boy, the character encoding in the ROM may need to be different than the source file encoding.
For example, the tiles used for uppercase letters may be placed starting at tile index 128, which differs from ASCII starting at 65.
.Pp
Character maps allow mapping strings to arbitrary sequences of numbers:
Character maps allow mapping strings or character literals to arbitrary sequences of numbers:
.Bd -literal -offset indent
CHARMAP "A", 42
CHARMAP ":)", 39
CHARMAP ':)', 39
CHARMAP "<br>", 13, 10
CHARMAP "&euro;", $20ac
CHARMAP '&euro;', $20ac
.Ed
.Pp
This would result in
@@ -1691,7 +1701,8 @@ $ rgbasm -o a.o a.asm
$ rgbasm -o b.o b.asm
$ rgbasm -o c.o c.asm
$ rgblink a.o b.o c.o
error: c.asm(2): Unknown symbol "LabelA"
error: Undefined symbol "LabelA"
at c.asm(2)
Linking failed with 1 error
.Ed
.Pp
@@ -1700,15 +1711,21 @@ Note also that only exported symbols will appear in symbol and map files produce
.Ss Purging symbols
.Ic PURGE
allows you to completely remove a symbol from the symbol table, as if it had never been defined.
.Bd -literal -offset indent
DEF value EQU 42
PURGE value
DEF value EQUS "I'm a string now"
ASSERT DEF(value)
PURGE value
ASSERT !DEF(value)
.Ed
.Pp
Be
.Em very
careful when purging symbols, especially labels, because it could result in unpredictable errors if something depends on the missing symbol (for example, expressions the linker needs to calculate).
.Bd -literal -offset indent
DEF Kamikaze EQUS "I don't want to live anymore"
AOLer: DB "Me too lol"
PURGE Kamikaze, AOLer
ASSERT !DEF(Kamikaze) && !DEF(AOLer)
.Ed
careful when purging symbols that have been referenced in section data, or that have been exported, because it could result in unpredictable errors if something depends on the missing symbol (for example, expressions the linker needs to calculate).
Purging labels at all is
.Em not
recommended.
.Pp
String constants are not expanded within the symbol names.
.Ss Predeclared symbols
@@ -1718,10 +1735,9 @@ The following symbols are defined by the assembler:
.It Dv @ Ta Ic EQU Ta PC value (essentially, the current memory address)
.It Dv . Ta Ic EQUS Ta The current global label scope
.It Dv .. Ta Ic EQUS Ta The current local label scope
.It Dv __SCOPE__ Ta Ic EQUS Ta The innermost current label scope level (empty, ".", or "..")
.It Dv _RS Ta Ic = Ta _RS Counter
.It Dv _NARG Ta Ic EQU Ta Number of arguments passed to macro, updated by Ic SHIFT
.It Dv __DATE__ Ta Ic EQUS Ta Today's date
.It Dv __TIME__ Ta Ic EQUS Ta The current time
.It Dv __ISO_8601_LOCAL__ Ta Ic EQUS Ta ISO 8601 timestamp (local)
.It Dv __ISO_8601_UTC__ Ta Ic EQUS Ta ISO 8601 timestamp (UTC)
.It Dv __UTC_YEAR__ Ta Ic EQU Ta Today's year
@@ -2494,7 +2510,8 @@ can be used to change some of the options during assembling from within the sour
takes a comma-separated list of options as its argument:
.Bd -literal -offset indent
PUSHO
OPT g.oOX, Wdiv ; acts like command-line -g.oOX -Wdiv
OPT g.oOX, Wdiv ; acts like command-line `-g.oOX -Wdiv`
OPT -Wdiv ; dashes before the options are optional
DW `..ooOOXX ; uses the graphics constant characters from OPT g
PRINTLN $80000000/-1 ; prints a warning about division
POPO
@@ -2527,6 +2544,68 @@ PUSHO b.X, g.oOX
DW `..ooOOXX
POPO
.Ed
.Ss Excluding locations from backtraces
Errors and warnings print
.Em backtraces
showing the location in the source file where the problem occurred, tracing the origin of the problem even through a chain of
.Ic REPT ,
.Ic FOR ,
.Ic MACRO ,
and
.Ic INCLUDE
locations.
Sometimes there are locations you would like to ignore; for example, a common utility macro when you only care about the line where the macro is used, or an
.Ic INCLUDE
file that only serves to include other files and is just filler in the backtrace.
.Pp
In those cases, you can
.Em silence
a location with a question mark
.Sq \&?
after the token: all of the locations created by a
.Sq REPT? ,
.Sq FOR? ,
or
.Sq MACRO?
will not be printed, and any location created by a
.Sq INCLUDE? ,
or a macro invocation whose name is immediately followed by a
.Sq \&? ,
will not be printed.
For example, if this were assembled as
.Ql example.asm :
.Bd -literal -offset indent
MACRO lb
assert -128 <= (\e2) && (\e2) < 256, "\e2 is not a byte"
assert -128 <= (\e3) && (\e3) < 256, "\e3 is not a byte"
ld \e1, (LOW(\e2) << 8) | LOW(\e3)
ENDM
SECTION "Code", ROM0
lb hl, $123, $45
.Ed
.Pp
This would print an error backtrace:
.Bd -literal -offset indent
error: Assertion failed: $123 is not a byte
at example.asm::lb(2)
<- example.asm(7)
.Ed
.Pp
But if
.Ql MACRO
were changed to
.Ql MACRO? ,
or
.Ql lb hl
were changed to
.Ql lb? hl ,
then the error backtrace would not mention the location within the
.Ql lb
macro:
.Bd -literal -offset indent
error: Assertion failed: $123 is not a byte
at example.asm(7)
.Ed
.Sh SEE ALSO
.Xr rgbasm 1 ,
.Xr rgblink 1 ,

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBDS 5
.Os
.Sh NAME
@@ -79,12 +79,16 @@ order, meaning the node with ID 0 is the last one in the list!
.It Cm LONG Ar ParentLineNo
Line at which the parent node's context was exited; meaningless for the root node.
.It Cm BYTE Ar Type
Bits 0\(en6 indicate the node's type:
.Bl -column "Value" -compact
.It Sy Value Ta Sy Meaning
.It 0 Ta REPT node
.It 1 Ta File node
.It 2 Ta Macro node
.El
.Pp
Bit\ 7 being set means that the node is "quieted"
.Pq see Do Excluding locations from backtraces Dc in Xr rgbasm 5 .
.It Cm IF Ar Type No \(!= 0
If the node is not a REPT node...
.Pp
@@ -401,9 +405,6 @@ Checks if the value is a valid bit index
.Pq see e.g. Do BIT u3, r8 Dc in Xr gbz80 7 ,
that is, from 0 to 7.
The value is then ORed with the instruction's mask.
.It Li $80 Ta Integer literal; followed by the
.Cm LONG
integer.
.It Li $70 Ta Cm HIGH
byte.
.It Li $71 Ta Cm LOW
@@ -412,6 +413,9 @@ byte.
value.
.It Li $73 Ta Cm TZCOUNT
value.
.It Li $80 Ta Integer literal; followed by the
.Cm LONG
integer.
.It Li $81 Ta A symbol's value; followed by the symbol's
.Cm LONG
ID.

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBDS 7
.Os
.Sh NAME

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBFIX 1
.Os
.Sh NAME
@@ -8,8 +8,9 @@
.Nd Game Boy header utility and checksum fixer
.Sh SYNOPSIS
.Nm
.Op Fl hjOsVvw
.Op Fl hjsVvw
.Op Fl C | c
.Op Fl \-color Ar when
.Op Fl f Ar fix_spec
.Op Fl i Ar game_id
.Op Fl k Ar licensee_str
@@ -22,7 +23,7 @@
.Op Fl r Ar ram_size
.Op Fl t Ar title_str
.Op Fl W Ar warning
.Op Ar
.Ar
.Sh DESCRIPTION
The
.Nm
@@ -38,23 +39,67 @@ Developers are advised to fill those fields with 0x00 bytes in their source code
.Nm ,
and to have already populated whichever fields they don't specify using
.Nm .
.Pp
The input
.Ar asmfile
can be a path to a file, or
.Cm \-
to read from standard input.
.Pp
Note that options can be abbreviated as long as the abbreviation is unambiguous:
.Fl \-color-o
.Sh ARGUMENTS
.Nm
accepts the usual short and long options, such as
.Fl V
and
.Fl -version .
Options later in the command line override those set earlier, except for when duplicate options are considered an error.
Options can be abbreviated as long as the abbreviation is unambiguous:
.Fl \-ver
is
.Fl \-color-only ,
.Fl \-version ,
but
.Fl \-color
.Fl \-v
is invalid because it could also be
.Fl \-color-compatible .
Options later in the command line override those set earlier.
Accepted options are as follows:
.Fl \-validate .
.Pp
Unless otherwise noted, passing
.Ql -
(a single dash) as a file name makes
.Nm
use standard input (for input files) or standard output (for output files).
To suppress this behavior, and open a file in the current directory actually called
.Ql - ,
pass
.Ql ./-
instead.
Using standard input or output for more than one file in a single command may produce unexpected results.
.Pp
.Nm
accepts decimal, hexadecimal, octal, and binary for numeric option arguments.
Decimal numbers are written as usual; hexadecimal numbers must be prefixed with either
.Ql $
or
.Ql 0x ;
octal numbers must be prefixed with either
.Ql &
or
.Ql 0o ;
and binary numbers must be prefixed with either
.Ql %
or
.Ql 0b .
(The prefixes
.Ql $
and
.Ql &
will likely need escaping or quoting to avoid being interpreted by the shell.)
Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive.
For example, all of these are equivalent:
.Ql 42 ,
.Ql 042 ,
.Ql 0x2A ,
.Ql 0X2A ,
.Ql 0x2a ,
.Ql &52 ,
.Ql 0o52 ,
.Ql 0O052 ,
.Ql 0b00101010 ,
.Ql 0B101010 .
.Pp
The following options are accepted:
.Bl -tag -width Ds
.It Fl C , Fl \-color-only
Set the Game Boy Color\(enonly flag
@@ -70,6 +115,18 @@ to 0x80.
This overrides
.Fl c
if it was set prior.
.It Fl \-color Ar when
Specify when to highlight warning and error messages with color:
.Ql always ,
.Ql never ,
or
.Ql auto .
.Ql auto
determines whether to use colors based on the
.Ql Lk https://no-color.org/ NO_COLOR
or
.Ql Lk https://force-color.org/ FORCE_COLOR
environment variables, or whether the output is to a TTY.
.It Fl f Ar fix_spec , Fl \-fix-spec Ar fix_spec
Fix certain header values that the Game Boy checks for correctness.
Alternatively, intentionally trash these values by writing their binary inverse instead.
@@ -136,9 +193,6 @@ section below.
Set the ROM version
.Pq Ad 0x14C
to a given value from 0 to 0xFF.
.It Fl O , Fl \-overwrite
Alias for
.Fl Wno-overwrite .
.It Fl o Ar out_file , Fl \-output Ar out_file
Write the modified ROM image to the given file, or '-' to write to standard output.
If not specified, the input files are modified in-place, or written to standard output if read from standard input.
@@ -159,7 +213,7 @@ Set the SGB flag
.Pq Ad 0x146
to 0x03.
This flag will be ignored by the SGB unless the old licensee code
.Pq Fl -l
.Pq Fl l
is 0x33!
.It Fl t Ar title , Fl \-title Ar title
Set the title string
@@ -189,6 +243,23 @@ See the
section for a list of warnings.
.It Fl w
Disable all warning output, even when turned into errors.
.It @ Ns Ar at_file
Read more options and arguments from a file, as if its contents were given on the command line.
Arguments are separated by whitespace or newlines.
Lines starting with a hash sign
.Pq Ql #
are considered comments and ignored.
.Pp
No shell processing is performed, such as wildcard or variable expansion.
There is no support for escaping or quoting whitespace to be included in arguments.
The standard
.Ql --
to stop option processing also disables at-file processing.
Note that while
.Ql --
can be used
.Em inside
an at-file, it only disables option processing within that at-file, and processing continues in the parent scope.
.El
.Sh DIAGNOSTICS
Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the header-fixing process.
@@ -202,7 +273,7 @@ to prevent turning all warnings into errors.
.It Fl Werror=
Make the specified warning or meta warning into an error.
A warning's name is appended
.Pq example: Fl Werror=overwrite ,
.Pq example: Fl Werror=obsolete ,
and this warning is implicitly enabled and turned into an error.
This can be negated as
.Fl Wno-error=
@@ -224,10 +295,10 @@ Enables literally every warning.
.El
.Pp
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 Wtruncation
Note that each of these flags also has a negation (for example,
.Fl Wobsolete
enables the warning that
.Fl Wno-truncation
.Fl Wno-obsolete
disables; and
.Fl Wall
enables every warning that
@@ -240,6 +311,8 @@ prefix, entries are listed alphabetically.
.Bl -tag -width Ds
.It Fl Wno-mbc
Warn when there are inconsistencies with or caveats about the specified MBC type.
.It Fl Wno-obsolete
Warn when obsolete features are encountered, which have been deprecated and may later be removed.
.It Fl Wno-overwrite
Warn when overwriting different non-zero bytes in the header.
.It Fl Wno-sgb
@@ -315,7 +388,7 @@ will warn about and ignore
.Fl j
if used in combination with TPP1.
.Sh BUGS
Please report bugs on
Please report bugs or mistakes in this documentation on
.Lk https://github.com/gbdev/rgbds/issues GitHub .
.Sh SEE ALSO
.Xr rgbasm 1 ,

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBGFX 1
.Os
.Sh NAME
@@ -15,6 +15,7 @@
.Op Fl a Ar attrmap | Fl A
.Op Fl b Ar base_ids
.Op Fl c Ar pal_spec
.Op Fl \-color Ar when
.Op Fl d Ar depth
.Op Fl i Ar input_tiles
.Op Fl L Ar slice
@@ -42,7 +43,13 @@ is to divide the input PNG into 8\[tmu]8 pixel
convert each of those squares into 1bpp or 2bpp tile data, and save all of the tile data in a file.
It also has options to generate a tile map, attribute map, and/or palette set as well; more on that and how the conversion process can be tweaked below.
.Sh ARGUMENTS
Note that options can be abbreviated as long as the abbreviation is unambiguous:
.Nm
accepts the usual short and long options, such as
.Fl V
and
.Fl -version .
Options later in the command line override those set earlier, except for when duplicate options are considered an error.
Options can be abbreviated as long as the abbreviation is unambiguous:
.Fl \-verb
is
.Fl \-verbose ,
@@ -51,26 +58,6 @@ but
is invalid because it could also be
.Fl \-version .
.Pp
.Nm
accepts decimal, binary, and hexadecimal numbers in option arguments.
Decimal numbers are written as usual; binary numbers must be prefixed with either
.Ql %
or
.Ql 0b ,
and hexadecimal numbers must be prefixed with either
.Ql $
(which will likely need escaping or quoting to avoid being interpreted by the shell), or
.Ql 0x .
Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive.
All of these are equivalent:
.Ql 42 ,
.Ql 042 ,
.Ql 0b00101010 ,
.Ql 0B101010 ,
.Ql 0x2A ,
.Ql 0X2A ,
.Ql 0x2a .
.Pp
Unless otherwise noted, passing
.Ql -
(a single dash) as a file name makes
@@ -81,7 +68,39 @@ To suppress this behavior, and open a file in the current directory actually cal
pass
.Ql ./-
instead.
Using standard input or output more than once in a single command will likely produce unexpected results.
Using standard input or output for more than one file in a single command may produce unexpected results.
.Pp
.Nm
accepts decimal, hexadecimal, octal, and binary for numeric option arguments.
Decimal numbers are written as usual; hexadecimal numbers must be prefixed with either
.Ql $
or
.Ql 0x ;
octal numbers must be prefixed with either
.Ql &
or
.Ql 0o ;
and binary numbers must be prefixed with either
.Ql %
or
.Ql 0b .
(The prefixes
.Ql $
and
.Ql &
will likely need escaping or quoting to avoid being interpreted by the shell.)
Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive.
For example, all of these are equivalent:
.Ql 42 ,
.Ql 042 ,
.Ql 0x2A ,
.Ql 0X2A ,
.Ql 0x2a ,
.Ql &52 ,
.Ql 0o52 ,
.Ql 0O052 ,
.Ql 0b00101010 ,
.Ql 0B101010 .
.Pp
The following options are accepted:
.Bl -tag -width Ds
@@ -177,8 +196,25 @@ the low two bits 0-1 specify which gray shade goes in color index 0,
the next two bits 2-3 specify which gray shade goes in color index 1,
and so on.
Gray shade 0 is the lightest (white), 3 is the darkest (black).
If
.Ar pal_spec
is the case-insensitive word
.Cm dmg ,
then it acts like
.Cm dmg=E4 ,
i.e. the darkest gray will end up in color index 0, and so on.
The same gray shade cannot go in two color indexes.
To specify a DMG palette, the input PNG must have all its colors in shades of gray, without any transparent colors.
.It Sy automatic palette generation
If
.Ar pal_spec
is the case-insensitive word
.Cm auto ,
then a palette is automatically generated using the procedure described in
.Sx PALETTE GENERATION .
This is the default behavior if
.Fl c
was not specified.
.It Sy external palette spec
Otherwise,
.Ar pal_spec
@@ -195,6 +231,18 @@ See
.Sx PALETTE SPECIFICATION FORMATS
for a list of formats and their descriptions.
.El
.It Fl \-color Ar when
Specify when to highlight warning and error messages with color:
.Ql always ,
.Ql never ,
or
.Ql auto .
.Ql auto
determines whether to use colors based on the
.Ql Lk https://no-color.org/ NO_COLOR
or
.Ql Lk https://force-color.org/ FORCE_COLOR
environment variables, or whether the output is to a TTY.
.It Fl d Ar depth , Fl \-depth Ar depth
Set the bit depth of the output tile data, in bits per pixel (bpp), either 1 or 2 (the default).
This changes how tile data is output, and the maximum number of colors per palette (2 and 4 respectively).
@@ -317,7 +365,7 @@ Output the image's palette set to this file.
Same as
.Fl p Ar base_path Ns .pal
.Pq see Sx Automatic output paths .
.It Fl q Ar pal_file , Fl \-palette-map Ar pal_file
.It Fl q Ar pal_map , Fl \-palette-map Ar pal_map
Output the image's palette map to this file.
This is useful if the input image contains more than 8 palettes, as the attribute map only contains the lower 3 bits of the palette indices.
.It Fl Q , Fl \-auto-palette-map
@@ -382,14 +430,17 @@ Be verbose.
The verbosity level is increased by one each time the flag is specified, with each level including the previous:
.Bl -enum -compact
.It
Print the
.Nm
prints out its configuration before doing anything.
configuration before taking actions.
.It
A generic message is printed before doing most actions.
Print a notice before significant actions.
.It
Some of the actions' intermediate results are printed.
Print some of the actions' intermediate results.
.It
Some internal debug printing is enabled.
Print some internal debug information.
.It
Print detailed internal information.
.El
The verbosity level does not go past 6.
.Pp
@@ -436,69 +487,53 @@ Implies
.It Fl Z , Fl \-columns
Read squares from the PNG in column-major order (column by column), instead of the default row-major order (line by line).
This primarily affects tile map and attribute map output, although it may also change generated tile data and palettes.
.It @ Ns Ar at_file
Read more options and arguments from a file, as if its contents were given on the command line.
Arguments are separated by whitespace or newlines.
Lines starting with a hash sign
.Pq Ql #
are considered comments and ignored.
.Pp
No shell processing is performed, such as wildcard or variable expansion.
There is no support for escaping or quoting whitespace to be included in arguments.
The standard
.Ql --
to stop option processing also disables at-file processing.
Note that while
.Ql --
can be used
.Em inside
an at-file, it only disables option processing within that at-file, and processing continues in the parent scope.
.Pp
See
.Sx At-files
below for an explanation of how this can be useful.
.El
.Ss At-files
In a given project, many images are to be converted with different flags.
The traditional way of solving this problem has been to specify the different flags for each image in the Makefile / build script; this can be inconvenient, as it centralizes all those flags away from the images they concern.
The traditional way of solving this problem has been to specify the different flags for each image in the Makefile or build script; this can be inconvenient, as it centralizes all those flags away from the images they concern.
.Pp
To avoid these drawbacks,
.Nm
supports
To avoid these drawbacks, you can use
.Dq at-files :
any command-line argument that begins with an at sign
.Pq Ql @
is interpreted as one.
The rest of the argument (without the @, that is) is interpreted as the path to a file, whose contents are interpreted as if given on the command line.
is interpreted as one, as documented above.
At-files can be stored right next to the corresponding image, for example:
.Pp
.Dl $ rgbgfx -o image.2bpp -t image.tilemap @image.flags image.png
.Pp
This will read additional flags from file
This will read additional flags from the file
.Ql image.flags ,
which could contains for example
which could contain, for example,
.Ql -b 128
to specify a base offset for the image's tiles.
The above command could be generated from the following
.Xr make 1
rule, for example:
rule:
.Bd -literal -offset indent
%.2bpp %.tilemap: %.flags %.png
rgbgfx -o $*.2bpp -t $*.tilemap @$*.flags $*.png
.Ed
.Pp
Since the contents of at-files are interpreted by
.Nm ,
.Sy no shell processing is performed ;
for example, shell variables are not expanded
.Ql ( $PWD ,
.Ql %WINDIR% ,
etc.).
In at-files, lines that are empty or contain only whitespace are ignored; lines that begin with a hash sign
.Pq Ql # ,
optionally preceded by whitespace, are considered comments and also ignored.
Each line can contain any number of arguments, which are separated by whitespace.
.Pq \&No quoting feature to prevent this is provided.
.Pp
Note that a leading
.Ql @
has no special meaning on option arguments, and that the standard
.Ql --
to stop option processing also disables at-file processing.
For example, the following command line reads command-line options from
.Ql tilesets/town.flags
then
.Ql tilesets.flags ,
but processes
.Ql @tilesets/town.png
as the input image and outputs tile data to
.Ql @tilesets/town.2bpp :
.Pp
.Dl $ rgbgfx -o @tilesets/town.2bpp @tilesets/town.flags @tilesets.flags -- @tilesets/town.png
.Pp
At-files can also specify the input image directly, and call for more at-files, both using the regular syntax.
Note that while
.Ql --
can be used in an at-file (with identical semantics), it is only effective inside of it\(emnormal option processing continues in the parent scope.
.Sh PALETTE SPECIFICATION FORMATS
The following formats are supported:
.Bl -tag -width Ds
@@ -600,9 +635,10 @@ behavior depends on an internal detail of how the PNG is saved, specifically its
chunk.
Since few image editors (such as GIMP) expose that detail, this behavior is only kept for compatibility and should be considered deprecated.
.Pp
It turns out that palette generation is an NP-complete problem, so
It turns out that palette generation is an NP-complete problem known as "pagination", so
.Nm
does not attempt to find the optimal solution, but instead to find a good one in a reasonable amount of time.
does not attempt to find the optimal solution, but instead uses an "overload-and-remove" heuristic to find a good one in a reasonable amount of time.
(There are no guarantees about how this algorithm will generate palettes, apart from the constraints documented above.)
It is possible to compute the optimal solution externally (using a solver, for example), and then provide it to
.Nm
via
@@ -735,7 +771,7 @@ to prevent turning all warnings into errors.
.It Fl Werror=
Make the specified warning or meta warning into an error.
A warning's name is appended
.Pq example: Fl Werror=embedded ,
.Pq example: Fl Werror=obsolete ,
and this warning is implicitly enabled and turned into an error.
This can be negated as
.Fl Wno-error=
@@ -757,10 +793,10 @@ Enables literally every warning.
.El
.Pp
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 Wtrim-nonempty
Note that each of these flags also has a negation (for example,
.Fl Wobsolete
enables the warning that
.Fl Wno-trim-nonempty
.Fl Wno-obsolete
disables; and
.Fl Wall
enables every warning that
@@ -777,6 +813,8 @@ Warn when a generated palette is sorted according to the input PNG's embedded pa
was not provided.
This warning is enabled by
.Fl Weverything .
.It Fl Wno-obsolete
Warn when obsolete features are encountered, which have been deprecated and may later be removed.
.It Fl Wtrim-nonempty
Warn when
.Fl x
@@ -824,9 +862,8 @@ $ rgbgfx level1.png -i tileset.2bpp -c gbc:tileset.pal -t level1.tilemap -a leve
$ rgbgfx level2.png -i tileset.2bpp -c gbc:tileset.pal -t level2.tilemap -a level2.attrmap
.Ed
.Sh BUGS
Please report bugs and mistakes in this man page on
Please report bugs or mistakes in this documentation on
.Lk https://github.com/gbdev/rgbds/issues GitHub .
Bug reports and feature requests about RGBDS are also welcome!
.Sh SEE ALSO
.Xr rgbasm 1 ,
.Xr rgblink 1 ,

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBLINK 1
.Os
.Sh NAME
@@ -9,6 +9,8 @@
.Sh SYNOPSIS
.Nm
.Op Fl dhMtVvwx
.Op Fl B Ar param
.Op Fl \-color Ar when
.Op Fl l Ar linker_script
.Op Fl m Ar map_file
.Op Fl n Ar sym_file
@@ -46,14 +48,14 @@ Also, if your ROM is designed for a monochrome Game Boy, you can make sure that
option, which implies
.Fl w
but also prohibits the use of banked VRAM.
.Pp
The input
.Ar asmfile
can be a path to a file, or
.Cm \-
to read from standard input.
.Pp
Note that options can be abbreviated as long as the abbreviation is unambiguous:
.Sh ARGUMENTS
.Nm
accepts the usual short and long options, such as
.Fl V
and
.Fl -version .
Options later in the command line override those set earlier, except for when duplicate options are considered an error.
Options can be abbreviated as long as the abbreviation is unambiguous:
.Fl \-verb
is
.Fl \-verbose ,
@@ -61,8 +63,88 @@ but
.Fl \-ver
is invalid because it could also be
.Fl \-version .
The arguments are as follows:
.Pp
Unless otherwise noted, passing
.Ql -
(a single dash) as a file name makes
.Nm
use standard input (for input files) or standard output (for output files).
To suppress this behavior, and open a file in the current directory actually called
.Ql - ,
pass
.Ql ./-
instead.
Using standard input or output for more than one file in a single command may produce unexpected results.
.Pp
.Nm
accepts decimal, hexadecimal, octal, and binary for numeric option arguments.
Decimal numbers are written as usual; hexadecimal numbers must be prefixed with either
.Ql $
or
.Ql 0x ;
octal numbers must be prefixed with either
.Ql &
or
.Ql 0o ;
and binary numbers must be prefixed with either
.Ql %
or
.Ql 0b .
(The prefixes
.Ql $
and
.Ql &
will likely need escaping or quoting to avoid being interpreted by the shell.)
Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive.
For example, all of these are equivalent:
.Ql 42 ,
.Ql 042 ,
.Ql 0x2A ,
.Ql 0X2A ,
.Ql 0x2a ,
.Ql &52 ,
.Ql 0o52 ,
.Ql 0O052 ,
.Ql 0b00101010 ,
.Ql 0B101010 .
.Pp
The following options are accepted:
.Bl -tag -width Ds
.It Fl B Ar param , Fl \-backtrace Ar param
Configures how location backtraces are printed if warnings or errors occur.
This flag may be specified multiple times with different parameters that combine meaningfully.
If
.Ar param
is a positive number, it specifies the maximum backtrace depth, abbreviating deeper ones.
Other valid parameter values are the following:
.Bl -tag -width Ds
.It Cm 0
Do not limit the maximum backtrace depth; this is the default.
.It Cm all
Force all locations to be printed, even "quiet" ones (see
.Dq Excluding locations from backtraces
in
.Xr rgbasm 5
for details).
.It Cm no-all
Do not print "quieted" locations in backtraces; this is the default.
.It Cm collapse
Print all locations on one line.
.It Cm no-collapse
Print one location per line; this is the default.
.El
.It Fl \-color Ar when
Specify when to highlight warning and error messages with color:
.Ql always ,
.Ql never ,
or
.Ql auto .
.Ql auto
determines whether to use colors based on the
.Ql Lk https://no-color.org/ NO_COLOR
or
.Ql Lk https://force-color.org/ FORCE_COLOR
environment variables, or whether the output is to a TTY.
.It Fl d , Fl \-dmg
Enable DMG mode.
Prohibit the use of sections that doesn't exist on a DMG, such as VRAM bank 1.
@@ -114,7 +196,25 @@ Useful for ROMs that fit in 32 KiB.
.It Fl V , Fl \-version
Print the version of the program and exit.
.It Fl v , Fl \-verbose
Verbose: enable printing more information to standard error.
Be verbose.
The verbosity level is increased by one each time the flag is specified, with each level including the previous:
.Bl -enum -compact
.It
Print the
.Nm
configuration before taking actions.
.It
Print a notice before significant actions.
.It
Print some of the actions' intermediate results.
.It
Print some internal debug information.
.It
Print detailed internal information.
.El
The verbosity level does not go past 6.
.Pp
Note that verbose output is only intended to be consumed by humans, and may change without notice between RGBDS releases; relying on those for scripts is not advised.
.It Fl W Ar warning , Fl \-warning Ar warning
Set warning flag
.Ar warning .
@@ -135,10 +235,32 @@ You can use this to make binary files that are not a ROM.
When making a ROM, note that not using this is not a replacement for
.Xr rgbfix 1 Ap s Fl p
option!
.It @ Ns Ar at_file
Read more options and arguments from a file, as if its contents were given on the command line.
Arguments are separated by whitespace or newlines.
Lines starting with a hash sign
.Pq Ql #
are considered comments and ignored.
.Pp
No shell processing is performed, such as wildcard or variable expansion.
There is no support for escaping or quoting whitespace to be included in arguments.
The standard
.Ql --
to stop option processing also disables at-file processing.
Note that while
.Ql --
can be used
.Em inside
an at-file, it only disables option processing within that at-file, and processing continues in the parent scope.
.El
.Ss Scrambling algorithm
The default section placement algorithm tries to minimize the number of banks used;
.Dq scrambling
The default section placement algorithm tries to place sections into as few banks as possible.
(It turns out that section placement is an NP-complete problem known as "bin packing", so
.Nm
does not attempt to find the optimal solution, but instead uses a "first-fit" heuristic to find a good one in a reasonable amount of time.
There are no guarantees about where this algorithm will place sections, apart from the bank, address, and alignment constraints manually specified for the sections.)
.Pp
.Dq Scrambling
instead places sections into a given pool of banks, trying to minimize the number of sections sharing a given bank.
This is useful to catch broken bank assumptions, such as expecting two different sections to land in the same bank (that is not guaranteed unless both are manually assigned the same bank number).
.Pp
@@ -198,7 +320,7 @@ to prevent turning all warnings into errors.
.It Fl Werror=
Make the specified warning or meta warning into an error.
A warning's name is appended
.Pq example: Fl Werror=assert ,
.Pq example: Fl Werror=obsolete ,
and this warning is implicitly enabled and turned into an error.
This can be negated as
.Fl Wno-error=
@@ -220,7 +342,7 @@ Enables literally every warning.
.El
.Pp
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,
Note that each of these flags also has a negation (for example,
.Fl Wobsolete
enables the warning that
.Fl Wno-obsolete
@@ -258,11 +380,20 @@ This warning is enabled by
Warn when a shift's operand is negative or greater than 32.
This warning is enabled by
.Fl Wall .
.It Fl Wno-truncation
.It Fl Wtruncation=
Warn when an implicit truncation (for example,
.Ic db
to an 8-bit value) loses some bits.
This occurs when an N-bit value is 2**N or greater, or less than -2**N.
.Fl Wtruncation=0
or
.Fl Wno-truncation
disables this warning.
.Fl Wtruncation=1
or just
.Fl Wtruncation
warns when an N-bit value is 2**N or greater, or less than -2**N.
.Fl Wtruncation=2
also warns when an N-bit value is less than -2**(N-1), which will not fit in two's complement encoding.
.El
.Sh EXAMPLES
All you need for a basic ROM is an object file, which can be made into a ROM image like so:
@@ -282,7 +413,7 @@ Here is a more complete example:
.Pp
.Dl $ rgblink -o bin/game.gb -n bin/game.sym -p 0xFF obj/title.o obj/engine.o
.Sh BUGS
Please report bugs on
Please report bugs or mistakes in this documentation on
.Lk https://github.com/gbdev/rgbds/issues GitHub .
.Sh SEE ALSO
.Xr rgbasm 1 ,

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 30, 2025
.Dt RGBLINK 5
.Os
.Sh NAME

View File

@@ -4,8 +4,11 @@ configure_file(version.cpp _version.cpp ESCAPE_QUOTES)
set(common_src
"extern/getopt.cpp"
"cli.cpp"
"diagnostics.cpp"
"style.cpp"
"usage.cpp"
"util.cpp"
"_version.cpp"
)
@@ -50,14 +53,16 @@ set(rgbasm_src
"asm/symbol.cpp"
"asm/warning.cpp"
"extern/utf8decoder.cpp"
"backtrace.cpp"
"linkdefs.cpp"
"opmath.cpp"
"util.cpp"
"verbosity.cpp"
)
set(rgblink_src
"${BISON_LINKER_SCRIPT_PARSER_OUTPUT_SOURCE}"
"link/assign.cpp"
"link/fstack.cpp"
"link/lexer.cpp"
"link/layout.cpp"
"link/main.cpp"
@@ -69,12 +74,14 @@ set(rgblink_src
"link/symbol.cpp"
"link/warning.cpp"
"extern/utf8decoder.cpp"
"backtrace.cpp"
"linkdefs.cpp"
"opmath.cpp"
"util.cpp"
"verbosity.cpp"
)
set(rgbfix_src
"fix/fix.cpp"
"fix/main.cpp"
"fix/mbc.cpp"
"fix/warning.cpp"
@@ -86,12 +93,13 @@ set(rgbgfx_src
"gfx/pal_packing.cpp"
"gfx/pal_sorting.cpp"
"gfx/pal_spec.cpp"
"gfx/palette.cpp"
"gfx/png.cpp"
"gfx/process.cpp"
"gfx/reverse.cpp"
"gfx/rgba.cpp"
"gfx/warning.cpp"
"util.cpp"
"verbosity.cpp"
)
foreach(PROG "asm" "fix" "gfx" "link")

View File

@@ -2,19 +2,137 @@
#include "asm/actions.hpp"
#include <errno.h>
#include <inttypes.h>
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include "extern/utf8decoder.hpp"
#include "helpers.hpp"
#include "linkdefs.hpp"
#include "asm/charmap.hpp"
#include "asm/format.hpp"
#include "asm/fstack.hpp"
#include "asm/lexer.hpp"
#include "asm/output.hpp"
#include "asm/rpn.hpp" // Expression
#include "asm/section.hpp"
#include "asm/symbol.hpp"
#include "asm/warning.hpp"
void act_If(int32_t condition) {
lexer_IncIFDepth();
if (condition) {
lexer_RunIFBlock();
} else {
lexer_SetMode(LEXER_SKIP_TO_ELIF);
}
}
void act_Elif(int32_t condition) {
if (lexer_GetIFDepth() == 0) {
fatal("Found `ELIF` outside of a conditional (not after an `IF`/`ELIF` block)");
}
if (lexer_RanIFBlock()) {
if (lexer_ReachedELSEBlock()) {
fatal("Found `ELIF` after an `ELSE` block");
}
// This should be redundant, as the lexer will have skipped to `ENDC` since
// an `ELIF` after a taken `IF` needs to not evaluate its condition.
lexer_SetMode(LEXER_SKIP_TO_ENDC); // LCOV_EXCL_LINE
} else if (condition) {
lexer_RunIFBlock();
} else {
lexer_SetMode(LEXER_SKIP_TO_ELIF);
}
}
void act_Else() {
if (lexer_GetIFDepth() == 0) {
fatal("Found `ELSE` outside of a conditional (not after an `IF`/`ELIF` block)");
}
if (lexer_RanIFBlock()) {
if (lexer_ReachedELSEBlock()) {
// This should be redundant, as the lexer handles this error first.
fatal("Found `ELSE` after an `ELSE` block"); // LCOV_EXCL_LINE
}
lexer_SetMode(LEXER_SKIP_TO_ENDC);
} else {
lexer_RunIFBlock();
lexer_ReachELSEBlock();
}
}
void act_Endc() {
lexer_DecIFDepth();
}
AlignmentSpec act_Alignment(int32_t alignment, int32_t alignOfs) {
AlignmentSpec spec = {0, 0};
if (alignment > 16) {
error("Alignment must be between 0 and 16, not %u", alignment);
} else if (alignOfs <= -(1 << alignment) || alignOfs >= 1 << alignment) {
error(
"The absolute alignment offset (%" PRIu32 ") must be less than alignment size (%d)",
static_cast<uint32_t>(alignOfs < 0 ? -alignOfs : alignOfs),
1 << alignment
);
} else {
spec.alignment = alignment;
spec.alignOfs = alignOfs < 0 ? (1 << alignment) + alignOfs : alignOfs;
}
return spec;
}
static void failAssert(AssertionType type, std::string const &message) {
switch (type) {
case ASSERT_FATAL:
if (message.empty()) {
fatal("Assertion failed");
} else {
fatal("Assertion failed: %s", message.c_str());
}
case ASSERT_ERROR:
if (message.empty()) {
error("Assertion failed");
} else {
error("Assertion failed: %s", message.c_str());
}
break;
case ASSERT_WARN:
if (message.empty()) {
warning(WARNING_ASSERT, "Assertion failed");
} else {
warning(WARNING_ASSERT, "Assertion failed: %s", message.c_str());
}
break;
}
}
void act_Assert(AssertionType type, Expression const &expr, std::string const &message) {
if (!expr.isKnown()) {
out_CreateAssert(type, expr, message, sect_GetOutputOffset());
} else if (expr.value() == 0) {
failAssert(type, message);
}
}
void act_StaticAssert(AssertionType type, int32_t condition, std::string const &message) {
if (!condition) {
failAssert(type, message);
}
}
std::optional<std::string> act_ReadFile(std::string const &name, uint32_t maxLen) {
FILE *file = nullptr;
if (std::optional<std::string> fullPath = fstk_FindFile(name); fullPath) {
@@ -38,46 +156,120 @@ std::optional<std::string> act_ReadFile(std::string const &name, uint32_t maxLen
readSize = fileSize;
}
fseek(file, 0, SEEK_SET);
// LCOV_EXCL_START
} else if (errno != ESPIPE) {
error("Error determining size of READFILE file '%s': %s", name.c_str(), strerror(errno));
error(
"Error determining size of `READFILE` file \"%s\": %s", name.c_str(), strerror(errno)
);
// LCOV_EXCL_STOP
}
std::string contents;
contents.resize(readSize);
if (fread(&contents[0], 1, readSize, file) < readSize || ferror(file)) {
error("Error reading READFILE file '%s': %s", name.c_str(), strerror(errno));
// LCOV_EXCL_START
error("Error reading `READFILE` file \"%s\": %s", name.c_str(), strerror(errno));
return "";
// LCOV_EXCL_STOP
}
return contents;
}
uint32_t act_StringToNum(std::vector<int32_t> const &str) {
uint32_t length = str.size();
uint32_t act_CharToNum(std::string const &str) {
if (std::vector<int32_t> units = charmap_Convert(str); units.size() == 1) {
// The string is a single character with a single unit value,
// which can be used directly as a numeric character.
return static_cast<uint32_t>(units[0]);
} else {
error("Character literals must be a single charmap unit");
return 0;
}
}
if (length == 1) {
// The string is a single character with a single value,
uint32_t act_StringToNum(std::string const &str) {
warning(
WARNING_OBSOLETE,
"Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead"
);
if (std::vector<int32_t> units = charmap_Convert(str); units.size() == 1) {
// The string is a single character with a single unit value,
// which can be used directly as a number.
return static_cast<uint32_t>(str[0]);
}
warning(WARNING_OBSOLETE, "Treating multi-unit strings as numbers is deprecated");
for (int32_t v : str) {
if (!checkNBit(v, 8, "All character units")) {
break;
return static_cast<uint32_t>(units[0]);
} else {
error("Strings as numbers must be a single charmap unit");
return 0;
}
}
uint32_t r = 0;
for (uint32_t i = length < 4 ? 0 : length - 4; i < length; ++i) {
r <<= 8;
r |= static_cast<uint8_t>(str[i]);
static uint32_t adjustNegativeIndex(int32_t idx, size_t len, char const *functionName) {
// String functions adjust negative index arguments the same way,
// such that position -1 is the last character of a string.
if (idx < 0) {
idx += len;
}
if (idx < 0) {
warning(WARNING_BUILTIN_ARG, "%s: Index starts at 0", functionName);
idx = 0;
}
return static_cast<uint32_t>(idx);
}
return r;
static uint32_t adjustNegativePos(int32_t pos, size_t len, char const *functionName) {
// STRSUB and CHARSUB adjust negative position arguments the same way,
// such that position -1 is the last character of a string.
if (pos < 0) {
pos += len + 1;
}
if (pos < 1) {
warning(WARNING_BUILTIN_ARG, "%s: Position starts at 1", functionName);
pos = 1;
}
return static_cast<uint32_t>(pos);
}
int32_t act_CharVal(std::string const &str) {
if (size_t len = charmap_CharSize(str); len == 0) {
error("CHARVAL: No character mapping for \"%s\"", str.c_str());
return 0;
} else if (len != 1) {
error("CHARVAL: Character mapping for \"%s\" must have a single value", str.c_str());
return 0;
} else {
return *charmap_CharValue(str, 0);
}
}
int32_t act_CharVal(std::string const &str, int32_t negIdx) {
if (size_t len = charmap_CharSize(str); len != 0) {
uint32_t idx = adjustNegativeIndex(negIdx, len, "CHARVAL");
if (std::optional<int32_t> val = charmap_CharValue(str, idx); val.has_value()) {
return *val;
} else {
warning(
WARNING_BUILTIN_ARG,
"CHARVAL: Index %" PRIu32 " is past the end of the character mapping",
idx
);
return 0;
}
} else {
error("CHARVAL: No character mapping for \"%s\"", str.c_str());
return 0;
}
}
uint8_t act_StringByte(std::string const &str, int32_t negIdx) {
size_t len = str.length();
if (uint32_t idx = adjustNegativeIndex(negIdx, len, "STRBYTE"); idx < len) {
return static_cast<uint8_t>(str[idx]);
} else {
warning(
WARNING_BUILTIN_ARG, "STRBYTE: Index %" PRIu32 " is past the end of the string", idx
);
return 0;
}
}
static void errorInvalidUTF8Byte(uint8_t byte, char const *functionName) {
@@ -116,7 +308,12 @@ size_t act_StringLen(std::string const &str, bool printErrors) {
return len;
}
std::string act_StringSlice(std::string const &str, uint32_t start, uint32_t stop) {
std::string
act_StringSlice(std::string const &str, int32_t negStart, std::optional<int32_t> negStop) {
size_t adjustLen = act_StringLen(str, false);
uint32_t start = adjustNegativeIndex(negStart, adjustLen, "STRSLICE");
uint32_t stop = negStop ? adjustNegativeIndex(*negStop, adjustLen, "STRSLICE") : adjustLen;
size_t strLen = str.length();
size_t index = 0;
uint32_t state = UTF8_ACCEPT;
@@ -180,7 +377,13 @@ std::string act_StringSlice(std::string const &str, uint32_t start, uint32_t sto
return str.substr(startIndex, index - startIndex);
}
std::string act_StringSub(std::string const &str, uint32_t pos, uint32_t len) {
std::string act_StringSub(std::string const &str, int32_t negPos, std::optional<uint32_t> optLen) {
warning(WARNING_OBSOLETE, "`STRSUB` is deprecated; use 0-indexed `STRSLICE` instead");
size_t adjustLen = act_StringLen(str, false);
uint32_t pos = adjustNegativePos(negPos, adjustLen, "STRSUB");
uint32_t len = optLen ? *optLen : pos > adjustLen ? 0 : adjustLen + 1 - pos;
size_t strLen = str.length();
size_t index = 0;
uint32_t state = UTF8_ACCEPT;
@@ -248,7 +451,10 @@ size_t act_CharLen(std::string const &str) {
return len;
}
std::string act_StringChar(std::string const &str, uint32_t idx) {
std::string act_StringChar(std::string const &str, int32_t negIdx) {
size_t adjustLen = act_CharLen(str);
uint32_t idx = adjustNegativeIndex(negIdx, adjustLen, "STRCHAR");
std::string_view view = str;
size_t charLen = 1;
@@ -269,7 +475,12 @@ std::string act_StringChar(std::string const &str, uint32_t idx) {
return std::string(start);
}
std::string act_CharSub(std::string const &str, uint32_t pos) {
std::string act_CharSub(std::string const &str, int32_t negPos) {
warning(WARNING_OBSOLETE, "`CHARSUB` is deprecated; use 0-indexed `STRCHAR` instead");
size_t adjustLen = act_CharLen(str);
uint32_t pos = adjustNegativePos(negPos, adjustLen, "CHARSUB");
std::string_view view = str;
size_t charLen = 1;
@@ -317,32 +528,6 @@ int32_t act_CharCmp(std::string_view str1, std::string_view str2) {
}
}
uint32_t act_AdjustNegativeIndex(int32_t idx, size_t len, char const *functionName) {
// String functions adjust negative index arguments the same way,
// such that position -1 is the last character of a string.
if (idx < 0) {
idx += len;
}
if (idx < 0) {
warning(WARNING_BUILTIN_ARG, "%s: Index starts at 0", functionName);
idx = 0;
}
return static_cast<uint32_t>(idx);
}
uint32_t act_AdjustNegativePos(int32_t pos, size_t len, char const *functionName) {
// STRSUB and CHARSUB adjust negative position arguments the same way,
// such that position -1 is the last character of a string.
if (pos < 0) {
pos += len + 1;
}
if (pos < 1) {
warning(WARNING_BUILTIN_ARG, "%s: Position starts at 1", functionName);
pos = 1;
}
return static_cast<uint32_t>(pos);
}
std::string
act_StringReplace(std::string_view str, std::string const &old, std::string const &rep) {
if (old.empty()) {
@@ -372,40 +557,30 @@ std::string act_StringFormat(
std::string str;
size_t argIndex = 0;
for (size_t i = 0; spec[i] != '\0'; ++i) {
int c = spec[i];
if (c != '%') {
for (size_t i = 0; spec[i] != '\0';) {
if (int c = spec[i]; c != '%') {
str += c;
++i;
continue;
}
c = spec[++i];
if (c == '%') {
if (int c = spec[++i]; c == '%') {
str += c;
++i;
continue;
}
FormatSpec fmt{};
while (c != '\0') {
fmt.useCharacter(c);
if (fmt.isFinished()) {
break;
}
c = spec[++i];
}
if (fmt.isEmpty()) {
} else if (c == '\0') {
error("STRFMT: Illegal '%%' at end of format string");
str += '%';
break;
}
FormatSpec fmt{};
size_t n = fmt.parseSpec(spec.c_str() + i);
i += n;
if (!fmt.isValid()) {
error("STRFMT: Invalid format spec for argument %zu", argIndex + 1);
str += '%';
str += spec.substr(i - n - 1, n + 1); // include the '%'
} else if (argIndex >= args.size()) {
// Will warn after formatting is done.
str += '%';
@@ -419,51 +594,43 @@ std::string act_StringFormat(
}
if (argIndex < args.size()) {
error("STRFMT: %zu unformatted argument(s)", args.size() - argIndex);
size_t extra = args.size() - argIndex;
error("STRFMT: %zu unformatted argument%s", extra, extra == 1 ? "" : "s");
} else if (argIndex > args.size()) {
error(
"STRFMT: Not enough arguments for format spec, got: %zu, need: %zu",
args.size(),
argIndex
"STRFMT: Not enough arguments for format spec (expected %zu, got %zu)",
argIndex,
args.size()
);
}
return str;
}
std::string act_SectionName(std::string const &symName) {
Symbol *sym = sym_FindScopedValidSymbol(symName);
if (!sym) {
if (sym_IsPurgedScoped(symName)) {
fatal("Undefined symbol `%s`; it was purged", symName.c_str());
} else {
fatal("Undefined symbol `%s`", symName.c_str());
}
}
Section const *section = sym->getSection();
if (!section) {
fatal("`%s` does not belong to any section", sym->name.c_str());
}
return section->name;
}
void act_CompoundAssignment(std::string const &symName, RPNCommand op, int32_t constValue) {
Expression oldExpr, constExpr, newExpr;
int32_t newValue;
oldExpr.makeSymbol(symName);
constExpr.makeNumber(constValue);
newExpr.makeBinaryOp(op, std::move(oldExpr), constExpr);
newValue = newExpr.getConstVal();
int32_t newValue = newExpr.getConstVal();
sym_AddVar(symName, newValue);
}
void act_FailAssert(AssertionType type) {
switch (type) {
case ASSERT_FATAL:
fatal("Assertion failed");
case ASSERT_ERROR:
error("Assertion failed");
break;
case ASSERT_WARN:
warning(WARNING_ASSERT, "Assertion failed");
break;
}
}
void act_FailAssertMsg(AssertionType type, std::string const &message) {
switch (type) {
case ASSERT_FATAL:
fatal("Assertion failed: %s", message.c_str());
case ASSERT_ERROR:
error("Assertion failed: %s", message.c_str());
break;
case ASSERT_WARN:
warning(WARNING_ASSERT, "Assertion failed: %s", message.c_str());
break;
}
}

View File

@@ -2,17 +2,21 @@
#include "asm/charmap.hpp"
#include <deque>
#include <map>
#include <optional>
#include <stack>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unordered_map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "extern/utf8decoder.hpp"
#include "helpers.hpp"
#include "itertools.hpp" // InsertionOrderedMap
#include "util.hpp"
#include "asm/warning.hpp"
@@ -34,8 +38,8 @@ struct Charmap {
};
// Traverse the trie depth-first to derive the character mappings in definition order
template<typename F>
bool forEachChar(Charmap const &charmap, F callback) {
template<typename CallbackFnT>
bool forEachChar(Charmap const &charmap, CallbackFnT callback) {
// clang-format off: nested initializers
for (std::stack<std::pair<size_t, std::string>> prefixes({{0, ""}}); !prefixes.empty();) {
// clang-format on
@@ -54,8 +58,7 @@ bool forEachChar(Charmap const &charmap, F callback) {
return true;
}
static std::deque<Charmap> charmapList;
static std::unordered_map<std::string, size_t> charmapMap; // Indexes into `charmapList`
static InsertionOrderedMap<Charmap> charmaps;
static Charmap *currentCharmap;
static std::stack<Charmap *> charmapStack;
@@ -64,7 +67,7 @@ bool charmap_ForEach(
void (*mapFunc)(std::string const &),
void (*charFunc)(std::string const &, std::vector<int32_t>)
) {
for (Charmap const &charmap : charmapList) {
for (Charmap const &charmap : charmaps) {
std::map<size_t, std::string> mappings;
forEachChar(charmap, [&mappings](size_t nodeIdx, std::string const &mapping) {
mappings[nodeIdx] = mapping;
@@ -72,49 +75,45 @@ bool charmap_ForEach(
});
mapFunc(charmap.name);
for (auto [nodeIdx, mapping] : mappings) {
for (auto const &[nodeIdx, mapping] : mappings) {
charFunc(mapping, charmap.nodes[nodeIdx].value);
}
}
return !charmapList.empty();
return !charmaps.empty();
}
void charmap_New(std::string const &name, std::string const *baseName) {
size_t baseIdx = SIZE_MAX;
std::optional<size_t> baseIdx = std::nullopt;
if (baseName != nullptr) {
if (auto search = charmapMap.find(*baseName); search == charmapMap.end()) {
error("Base charmap '%s' doesn't exist", baseName->c_str());
} else {
baseIdx = search->second;
baseIdx = charmaps.findIndex(*baseName);
if (!baseIdx) {
error("Undefined base charmap `%s`", baseName->c_str());
}
}
if (charmapMap.find(name) != charmapMap.end()) {
error("Charmap '%s' already exists", name.c_str());
if (charmaps.contains(name)) {
error("Charmap `%s` is already defined", name.c_str());
return;
}
// Init the new charmap's fields
charmapMap[name] = charmapList.size();
Charmap &charmap = charmapList.emplace_back();
if (baseIdx != SIZE_MAX) {
charmap.nodes = charmapList[baseIdx].nodes; // Copies `charmapList[baseIdx].nodes`
Charmap &charmap = charmaps.add(name);
charmap.name = name;
if (baseIdx) {
charmap.nodes = charmaps[*baseIdx].nodes; // Copies `charmaps[*baseIdx].nodes`
} else {
charmap.nodes.emplace_back(); // Zero-init the root node
}
charmap.name = name;
currentCharmap = &charmap;
}
void charmap_Set(std::string const &name) {
if (auto search = charmapMap.find(name); search == charmapMap.end()) {
error("Charmap '%s' doesn't exist", name.c_str());
if (auto index = charmaps.findIndex(name); index) {
currentCharmap = &charmaps[*index];
} else {
currentCharmap = &charmapList[search->second];
error("Undefined charmap `%s`", name.c_str());
}
}
@@ -289,7 +288,7 @@ size_t charmap_ConvertNext(std::string_view &input, std::vector<int32_t> *output
} else if (charmap.name != DEFAULT_CHARMAP_NAME) {
warning(
WARNING_UNMAPPED_CHAR_2,
"Unmapped character %s not in " DEFAULT_CHARMAP_NAME " charmap",
"Unmapped character %s not in `" DEFAULT_CHARMAP_NAME "` charmap",
printChar(firstChar)
);
}

View File

@@ -5,10 +5,10 @@
#include "asm/fixpoint.hpp"
#include <math.h>
#include <numbers>
#include <stdint.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
static constexpr double tau = std::numbers::pi * 2;
static double fix2double(int32_t i, int32_t q) {
return i / pow(2.0, q);
@@ -25,11 +25,11 @@ static int32_t double2fix(double d, int32_t q) {
}
static double turn2rad(double t) {
return t * (M_PI * 2);
return t * tau;
}
static double rad2turn(double r) {
return r / (M_PI * 2);
return r / tau;
}
int32_t fix_Sin(int32_t i, int32_t q) {

View File

@@ -5,96 +5,65 @@
#include <algorithm>
#include <inttypes.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include "util.hpp" // parseNumber
#include "asm/fixpoint.hpp"
#include "asm/main.hpp" // options
#include "asm/warning.hpp"
void FormatSpec::useCharacter(int c) {
if (state == FORMAT_INVALID) {
return;
}
size_t FormatSpec::parseSpec(char const *spec) {
size_t i = 0;
switch (c) {
// sign
case ' ':
case '+':
if (state > FORMAT_SIGN) {
break;
}
state = FORMAT_EXACT;
auto parseSpecNumber = [&spec, &i]() {
char const *end = &spec[i];
size_t number = parseNumber(end, BASE_10).value_or(0);
i += end - &spec[i];
return number;
};
// <sign>
if (char c = spec[i]; c == ' ' || c == '+') {
++i;
sign = c;
return;
// exact
case '#':
if (state > FORMAT_EXACT) {
break;
}
state = FORMAT_ALIGN;
// <exact>
if (spec[i] == '#') {
++i;
exact = true;
return;
// align
case '-':
if (state > FORMAT_ALIGN) {
break;
}
state = FORMAT_WIDTH;
// <align>
if (spec[i] == '-') {
++i;
alignLeft = true;
return;
// pad, width, and prec values
case '0':
if (state < FORMAT_WIDTH) {
}
// <pad>
if (spec[i] == '0') {
++i;
padZero = true;
}
[[fallthrough]];
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (state < FORMAT_WIDTH) {
state = FORMAT_WIDTH;
width = c - '0';
} else if (state == FORMAT_WIDTH) {
width = width * 10 + (c - '0');
} else if (state == FORMAT_FRAC) {
fracWidth = fracWidth * 10 + (c - '0');
} else if (state == FORMAT_PREC) {
precision = precision * 10 + (c - '0');
} else {
break;
// <width>
if (isDigit(spec[i])) {
width = parseSpecNumber();
}
return;
// width
case '.':
if (state > FORMAT_WIDTH) {
break;
}
state = FORMAT_FRAC;
// <frac>
if (spec[i] == '.') {
++i;
hasFrac = true;
return;
// prec
case 'q':
if (state > FORMAT_PREC) {
break;
fracWidth = parseSpecNumber();
}
state = FORMAT_PREC;
// <prec>
if (spec[i] == 'q') {
++i;
hasPrec = true;
return;
// type
precision = parseSpecNumber();
}
// <type>
switch (char c = spec[i]; c) {
case 'd':
case 'u':
case 'X':
@@ -103,26 +72,13 @@ void FormatSpec::useCharacter(int c) {
case 'o':
case 'f':
case 's':
if (state >= FORMAT_DONE) {
break;
}
state = FORMAT_DONE;
valid = true;
++i;
type = c;
return;
default:
break;
}
state = FORMAT_INVALID;
valid = false;
}
void FormatSpec::finishCharacters() {
if (!isValid()) {
state = FORMAT_INVALID;
}
// Done parsing
parsed = true;
return i;
}
static std::string escapeString(std::string const &str) {
@@ -157,7 +113,7 @@ static std::string escapeString(std::string const &str) {
void FormatSpec::appendString(std::string &str, std::string const &value) const {
int useType = type;
if (isEmpty()) {
if (!useType) {
// No format was specified
useType = 's';
}
@@ -196,7 +152,7 @@ void FormatSpec::appendString(std::string &str, std::string const &value) const
void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
int useType = type;
bool useExact = exact;
if (isEmpty()) {
if (!useType) {
// No format was specified; default to uppercase $hex
useType = 'X';
useExact = true;
@@ -221,36 +177,32 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
if (useType == 'd' || useType == 'f') {
if (int32_t v = value; v < 0) {
signChar = '-';
if (v != INT32_MIN) {
if (v != INT32_MIN) { // -INT32_MIN is UB
value = -v;
}
}
}
char prefixChar = !useExact ? 0
: useType == 'X' ? '$'
: useType == 'x' ? '$'
: useType == 'b' ? '%'
: useType == 'o' ? '&'
: 0;
char valueBuf[262]; // Max 5 digits + decimal + 255 fraction digits + terminator
// The longest possible formatted number is fixed-point with 10 digits, 255 fractional digits,
// and a precision suffix, for 270 total bytes (counting the NUL terminator).
// (Actually 269 since a 2-digit precision cannot reach 10 integer digits.)
// Make the buffer somewhat larger just in case.
char valueBuf[300];
if (useType == 'b') {
// Special case for binary
char *ptr = valueBuf;
// Special case for binary (since `snprintf` doesn't support it)
// Buffer the digits from least to greatest
char *ptr = valueBuf;
do {
*ptr++ = (value & 1) + '0';
value >>= 1;
} while (value);
// Reverse the digits
// Reverse the digits and terminate the string
std::reverse(valueBuf, ptr);
*ptr = '\0';
} else if (useType == 'f') {
// Special case for fixed-point
// Special case for fixed-point (since it needs fractional part and precision)
// Default fractional width (C++'s is 6 for "%f"; here 5 is enough for Q16.16)
size_t useFracWidth = hasFrac ? fracWidth : 5;
@@ -259,6 +211,7 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
useFracWidth = 255;
}
// Default precision taken from default `-Q` option
size_t defaultPrec = options.fixPrecision;
size_t usePrec = hasPrec ? precision : defaultPrec;
if (usePrec < 1 || usePrec > 31) {
@@ -270,29 +223,30 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
usePrec = defaultPrec;
}
// Floating-point formatting works for all fixed-point values
double fval = fabs(value / pow(2.0, usePrec));
if (int fracWidthArg = static_cast<int>(useFracWidth); useExact) {
snprintf(valueBuf, sizeof(valueBuf), "%.*fq%zu", fracWidthArg, fval, usePrec);
} else {
snprintf(valueBuf, sizeof(valueBuf), "%.*f", fracWidthArg, fval);
}
} else if (useType == 'd') {
// Decimal numbers may be formatted with a '-' sign by `snprintf`, so `abs` prevents that,
// with a special case for `INT32_MIN` since `labs(INT32_MIN)` is UB. The sign will be
// printed later from `signChar`.
uint32_t uval =
value != static_cast<uint32_t>(INT32_MIN) ? labs(static_cast<int32_t>(value)) : value;
snprintf(valueBuf, sizeof(valueBuf), "%" PRIu32, uval);
} else {
char const *spec = useType == 'u' ? "%" PRIu32
// `value` has already been made non-negative, so type 'd' is OK here even for `INT32_MIN`.
// The sign will be printed later from `signChar`.
char const *spec = useType == 'd' || useType == 'u' ? "%" PRIu32
: useType == 'X' ? "%" PRIX32
: useType == 'x' ? "%" PRIx32
: useType == 'o' ? "%" PRIo32
: "%" PRIu32;
snprintf(valueBuf, sizeof(valueBuf), spec, value);
}
char prefixChar = !useExact ? 0
: useType == 'X' || useType == 'x' ? '$'
: useType == 'b' ? '%'
: useType == 'o' ? '&'
: 0;
size_t valueLen = strlen(valueBuf);
size_t numLen = (signChar != 0) + (prefixChar != 0) + valueLen;
size_t totalLen = width > numLen ? width : numLen;

View File

@@ -7,14 +7,23 @@
#include <errno.h>
#include <inttypes.h>
#include <memory>
#include <optional>
#include <stack>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <utility>
#include <variant>
#include <vector>
#include "diagnostics.hpp"
#include "backtrace.hpp"
#include "helpers.hpp"
#include "itertools.hpp" // reversed
#include "linkdefs.hpp"
#include "platform.hpp" // S_ISDIR (stat macro)
#include "platform.hpp" // strncasecmp
#include "verbosity.hpp"
#include "asm/lexer.hpp"
#include "asm/macro.hpp"
@@ -48,45 +57,78 @@ static std::vector<std::string> includePaths = {""}; // -I
static std::deque<std::string> preIncludeNames; // -P
static bool failedOnMissingInclude = false;
std::string FileStackNode::reptChain() const {
std::string chain;
std::vector<uint32_t> const &nodeIters = iters();
for (uint32_t i = nodeIters.size(); i--;) {
chain.append("::REPT~");
chain.append(std::to_string(nodeIters[i]));
using TraceNode = std::pair<std::string, uint32_t>;
static std::vector<TraceNode> backtrace(FileStackNode const &node, uint32_t curLineNo) {
if (node.isQuiet && !tracing.loud) {
if (node.parent) {
// Quiet REPT nodes will pass their interior line number up to their parent,
// which is more precise than the parent's own line number (since that will be
// the line number of the "REPT?" or "FOR?" itself).
return backtrace(*node.parent, node.type == NODE_REPT ? curLineNo : node.lineNo);
}
return chain;
return {}; // LCOV_EXCL_LINE
}
std::string const &FileStackNode::dump(uint32_t curLineNo) const {
if (std::holds_alternative<std::vector<uint32_t>>(data)) {
assume(parent); // REPT nodes use their parent's name
std::string const &lastName = parent->dump(lineNo);
fputs(" -> ", stderr);
fputs(lastName.c_str(), stderr);
fputs(reptChain().c_str(), stderr);
fprintf(stderr, "(%" PRIu32 ")", curLineNo);
return lastName;
if (!node.parent) {
assume(node.type != NODE_REPT && std::holds_alternative<std::string>(node.data));
return {
{node.name(), curLineNo}
};
}
std::vector<TraceNode> traceNodes = backtrace(*node.parent, node.lineNo);
if (std::holds_alternative<std::vector<uint32_t>>(node.data)) {
assume(!traceNodes.empty()); // REPT nodes use their parent's name
std::string reptName = traceNodes.back().first;
if (std::vector<uint32_t> const &nodeIters = node.iters(); !nodeIters.empty()) {
reptName.append(NODE_SEPARATOR REPT_NODE_PREFIX);
reptName.append(std::to_string(nodeIters.front()));
}
traceNodes.emplace_back(reptName, curLineNo);
} else {
if (parent) {
parent->dump(lineNo);
fputs(" -> ", stderr);
}
std::string const &nodeName = name();
fputs(nodeName.c_str(), stderr);
fprintf(stderr, "(%" PRIu32 ")", curLineNo);
return nodeName;
traceNodes.emplace_back(node.name(), curLineNo);
}
return traceNodes;
}
bool fstk_DumpCurrent() {
if (lexer_AtTopLevel()) {
return false;
void FileStackNode::printBacktrace(uint32_t curLineNo) const {
trace_PrintBacktrace(
backtrace(*this, curLineNo),
[](TraceNode const &node) { return node.first.c_str(); },
[](TraceNode const &node) { return node.second; }
);
}
void fstk_TraceCurrent() {
if (!lexer_AtTopLevel()) {
assume(!contextStack.empty());
contextStack.top().fileInfo->dump(lexer_GetLineNo());
return true;
contextStack.top().fileInfo->printBacktrace(lexer_GetLineNo());
}
lexer_TraceStringExpansions();
}
// LCOV_EXCL_START
void fstk_VerboseOutputConfig() {
assume(checkVerbosity(VERB_CONFIG));
// -I/--include
if (includePaths.size() > 1) {
fputs("\tInclude file paths:\n", stderr);
for (std::string const &path : includePaths) {
if (!path.empty()) {
fprintf(stderr, "\t - %s\n", path.c_str());
}
}
}
// -P/--preinclude
if (!preIncludeNames.empty()) {
fputs("\tPreincluded files:\n", stderr);
for (std::string const &name : preIncludeNames) {
fprintf(stderr, "\t - %s\n", name.c_str());
}
}
}
// LCOV_EXCL_STOP
std::shared_ptr<FileStackNode> fstk_GetFileStack() {
return contextStack.empty() ? nullptr : contextStack.top().fileInfo;
@@ -124,7 +166,6 @@ void fstk_AddIncludePath(std::string const &path) {
void fstk_AddPreIncludeFile(std::string const &path) {
preIncludeNames.emplace_front(path);
verbosePrint("Pre-included filename %s\n", path.c_str()); // LCOV_EXCL_LINE
}
static bool isValidFilePath(std::string const &path) {
@@ -159,8 +200,9 @@ bool yywrap() {
if (ifDepth != 0) {
fatal(
"Ended block with %" PRIu32 " unterminated IF construct%s",
"Ended block with %" PRIu32 " unterminated conditional%s (`IF`/`ELIF`/`ELSE` block%s)",
ifDepth,
ifDepth == 1 ? "" : "s",
ifDepth == 1 ? "" : "s"
);
}
@@ -187,7 +229,7 @@ bool yywrap() {
// This error message will refer to the current iteration
if (sym->type != SYM_VAR) {
fatal("Failed to update FOR symbol value");
fatal("Failed to update `FOR` symbol value");
}
}
// Advance to the next iteration
@@ -214,14 +256,14 @@ static void checkRecursionDepth() {
}
}
static void newFileContext(std::string const &filePath, bool updateStateNow) {
static void newFileContext(std::string const &filePath, bool isQuiet, bool updateStateNow) {
checkRecursionDepth();
std::shared_ptr<std::string> uniqueIDStr = nullptr;
std::shared_ptr<MacroArgs> macroArgs = nullptr;
auto fileInfo =
std::make_shared<FileStackNode>(NODE_FILE, filePath == "-" ? "<stdin>" : filePath);
std::make_shared<FileStackNode>(NODE_FILE, filePath == "-" ? "<stdin>" : filePath, isQuiet);
if (!contextStack.empty()) {
Context &oldContext = contextStack.top();
fileInfo->parent = oldContext.fileInfo;
@@ -239,7 +281,8 @@ static void newFileContext(std::string const &filePath, bool updateStateNow) {
context.lexerState.setFileAsNextState(filePath, updateStateNow);
}
static void newMacroContext(Symbol const &macro, std::shared_ptr<MacroArgs> macroArgs) {
static void
newMacroContext(Symbol const &macro, std::shared_ptr<MacroArgs> macroArgs, bool isQuiet) {
checkRecursionDepth();
Context &oldContext = contextStack.top();
@@ -252,12 +295,16 @@ static void newMacroContext(Symbol const &macro, std::shared_ptr<MacroArgs> macr
}
}
if (macro.src->type == NODE_REPT) {
fileInfoName.append(macro.src->reptChain());
std::vector<uint32_t> const &srcIters = macro.src->iters();
for (uint32_t iter : reversed(srcIters)) {
fileInfoName.append(NODE_SEPARATOR REPT_NODE_PREFIX);
fileInfoName.append(std::to_string(iter));
}
fileInfoName.append("::");
}
fileInfoName.append(NODE_SEPARATOR);
fileInfoName.append(macro.name);
auto fileInfo = std::make_shared<FileStackNode>(NODE_MACRO, fileInfoName);
auto fileInfo = std::make_shared<FileStackNode>(NODE_MACRO, fileInfoName, isQuiet);
assume(!contextStack.empty()); // The top level context cannot be a MACRO
fileInfo->parent = oldContext.fileInfo;
fileInfo->lineNo = lexer_GetLineNo();
@@ -271,7 +318,8 @@ static void newMacroContext(Symbol const &macro, std::shared_ptr<MacroArgs> macr
context.lexerState.setViewAsNextState("MACRO", macro.getMacro(), macro.fileLine);
}
static Context &newReptContext(int32_t reptLineNo, ContentSpan const &span, uint32_t count) {
static Context &
newReptContext(int32_t reptLineNo, ContentSpan const &span, uint32_t count, bool isQuiet) {
checkRecursionDepth();
Context &oldContext = contextStack.top();
@@ -282,7 +330,7 @@ static Context &newReptContext(int32_t reptLineNo, ContentSpan const &span, uint
fileInfoIters.insert(fileInfoIters.end(), RANGE(oldContext.fileInfo->iters()));
}
auto fileInfo = std::make_shared<FileStackNode>(NODE_REPT, fileInfoIters);
auto fileInfo = std::make_shared<FileStackNode>(NODE_REPT, fileInfoIters, isQuiet);
assume(!contextStack.empty()); // The top level context cannot be a REPT
fileInfo->parent = oldContext.fileInfo;
fileInfo->lineNo = reptLineNo;
@@ -302,13 +350,17 @@ static Context &newReptContext(int32_t reptLineNo, ContentSpan const &span, uint
bool fstk_FileError(std::string const &path, char const *functionName) {
if (options.missingIncludeState == INC_ERROR) {
error("Error opening %s file '%s': %s", functionName, path.c_str(), strerror(errno));
error("Error opening `%s` file \"%s\": %s", functionName, path.c_str(), strerror(errno));
} else {
failedOnMissingInclude = true;
// LCOV_EXCL_START
if (options.missingIncludeState == GEN_EXIT) {
verbosePrint(
"Aborting (-MG) on %s file '%s' (%s)\n", functionName, path.c_str(), strerror(errno)
VERB_NOTICE,
"Aborting due to '-MG' on `%s` file \"%s\": %s\n",
functionName,
path.c_str(),
strerror(errno)
);
return true;
}
@@ -322,39 +374,61 @@ bool fstk_FailedOnMissingInclude() {
return failedOnMissingInclude;
}
bool fstk_RunInclude(std::string const &path) {
bool fstk_RunInclude(std::string const &path, bool isQuiet) {
if (std::optional<std::string> fullPath = fstk_FindFile(path); fullPath) {
newFileContext(*fullPath, false);
newFileContext(*fullPath, isQuiet, false);
return false;
}
return fstk_FileError(path, "INCLUDE");
}
void fstk_RunMacro(std::string const &macroName, std::shared_ptr<MacroArgs> macroArgs) {
Symbol *macro = sym_FindExactSymbol(macroName);
void fstk_RunMacro(
std::string const &macroName, std::shared_ptr<MacroArgs> macroArgs, bool isQuiet
) {
auto makeSuggestion = [&macroName, &macroArgs]() -> std::optional<std::string> {
std::shared_ptr<std::string> arg = macroArgs->getArg(1);
if (!arg) {
return std::nullopt;
}
if (!macro) {
char const *str = arg->c_str();
static char const *types[] = {"EQUS", "EQU", "RB", "RW", "RL", "="};
for (char const *type : types) {
if (strncasecmp(str, type, strlen(type)) == 0) {
return "\"DEF "s + macroName + " " + type + " ...\"";
}
}
if (strncasecmp(str, "SET", literal_strlen("SET")) == 0) {
return "\"DEF "s + macroName + " = ...\"";
}
if (str[0] == ':') {
return "a label \""s + macroName + (str[1] == ':' ? "::" : ":") + "\"";
}
return std::nullopt;
};
if (Symbol *macro = sym_FindExactSymbol(macroName); !macro) {
if (sym_IsPurgedExact(macroName)) {
error("Macro \"%s\" not defined; it was purged", macroName.c_str());
error("Undefined macro `%s`; it was purged", macroName.c_str());
} else if (std::optional<std::string> suggestion = makeSuggestion(); suggestion) {
error(
"Undefined macro `%s` (did you mean %s?)", macroName.c_str(), suggestion->c_str()
);
} else {
error("Macro \"%s\" not defined", macroName.c_str());
error("Undefined macro `%s`", macroName.c_str());
}
return;
} else if (macro->type != SYM_MACRO) {
error("`%s` is not a macro", macroName.c_str());
} else {
newMacroContext(*macro, macroArgs, isQuiet || macro->isQuiet);
}
if (macro->type != SYM_MACRO) {
error("\"%s\" is not a macro", macroName.c_str());
return;
}
newMacroContext(*macro, macroArgs);
void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span, bool isQuiet) {
if (count) {
newReptContext(reptLineNo, span, count, isQuiet);
}
void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span) {
if (count == 0) {
return;
}
newReptContext(reptLineNo, span, count);
}
void fstk_RunFor(
@@ -363,7 +437,8 @@ void fstk_RunFor(
int32_t stop,
int32_t step,
int32_t reptLineNo,
ContentSpan const &span
ContentSpan const &span,
bool isQuiet
) {
if (Symbol *sym = sym_AddVar(symName, start); sym->type != SYM_VAR) {
return;
@@ -375,18 +450,20 @@ void fstk_RunFor(
} else if (step < 0 && stop < start) {
count = (static_cast<int64_t>(start) - stop - 1) / -static_cast<int64_t>(step) + 1;
} else if (step == 0) {
error("FOR cannot have a step value of 0");
error("`FOR` cannot have a step value of 0");
}
if ((step > 0 && start > stop) || (step < 0 && start < stop)) {
warning(WARNING_BACKWARDS_FOR, "FOR goes backwards from %d to %d by %d", start, stop, step);
warning(
WARNING_BACKWARDS_FOR, "`FOR` goes backwards from %d to %d by %d", start, stop, step
);
}
if (count == 0) {
return;
}
Context &context = newReptContext(reptLineNo, span, count);
Context &context = newReptContext(reptLineNo, span, count, isQuiet);
context.isForLoop = true;
context.forValue = start;
context.forStep = step;
@@ -395,7 +472,7 @@ void fstk_RunFor(
bool fstk_Break() {
if (contextStack.top().fileInfo->type != NODE_REPT) {
error("BREAK can only be used inside a REPT/FOR block");
error("`BREAK` can only be used inside a loop (`REPT`/`FOR` block)");
return false;
}
@@ -411,13 +488,13 @@ void fstk_NewRecursionDepth(size_t newDepth) {
}
void fstk_Init(std::string const &mainPath) {
newFileContext(mainPath, true);
newFileContext(mainPath, false, true);
for (std::string const &name : preIncludeNames) {
if (std::optional<std::string> fullPath = fstk_FindFile(name); fullPath) {
newFileContext(*fullPath, false);
newFileContext(*fullPath, false, false);
} else {
error("Error reading pre-included file '%s': %s", name.c_str(), strerror(errno));
error("Error reading pre-included file \"%s\": %s", name.c_str(), strerror(errno));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
#include "asm/macro.hpp"
#include <memory>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <string>

View File

@@ -4,32 +4,112 @@
#include <algorithm>
#include <errno.h>
#include <limits.h>
#include <inttypes.h>
#include <memory>
#include <optional>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <time.h>
#include <unordered_map>
#include <utility>
#include <vector>
#include "backtrace.hpp"
#include "cli.hpp"
#include "diagnostics.hpp"
#include "extern/getopt.hpp"
#include "helpers.hpp"
#include "parser.hpp" // Generated from parser.y
#include "platform.hpp"
#include "style.hpp"
#include "usage.hpp"
#include "util.hpp" // UpperMap
#include "verbosity.hpp"
#include "version.hpp"
#include "asm/charmap.hpp"
#include "asm/fstack.hpp"
#include "asm/opt.hpp"
#include "asm/output.hpp"
#include "asm/section.hpp"
#include "asm/symbol.hpp"
#include "asm/warning.hpp"
Options options;
// Escapes Make-special chars from a string
static std::string make_escape(std::string &str) {
// Flags which must be processed after the option parsing finishes
static struct LocalOptions {
std::optional<std::string> dependFileName; // -M
std::unordered_map<std::string, std::vector<StateFeature>> stateFileSpecs; // -s
std::optional<std::string> inputFileName; // <file>
} localOptions;
// Short options
static char const *optstring = "B:b:D:Eg:hI:M:o:P:p:Q:r:s:VvW:wX:";
// Long-only option variable
static int longOpt; // `--color` and variants of `-M`
// Equivalent long options
// Please keep in the same order as short opts.
// Also, make sure long opts don't create ambiguity:
// A long opt's name should start with the same letter as its short opt,
// except if it doesn't create any ambiguity (`verbose` versus `version`).
// This is because long opt matching, even to a single char, is prioritized
// over short opt matching.
static option const longopts[] = {
{"backtrace", required_argument, nullptr, 'B'},
{"binary-digits", required_argument, nullptr, 'b'},
{"define", required_argument, nullptr, 'D'},
{"export-all", no_argument, nullptr, 'E'},
{"gfx-chars", required_argument, nullptr, 'g'},
{"help", no_argument, nullptr, 'h'},
{"include", required_argument, nullptr, 'I'},
{"dependfile", required_argument, nullptr, 'M'},
{"output", required_argument, nullptr, 'o'},
{"preinclude", required_argument, nullptr, 'P'},
{"pad-value", required_argument, nullptr, 'p'},
{"q-precision", required_argument, nullptr, 'Q'},
{"recursion-depth", required_argument, nullptr, 'r'},
{"state", required_argument, nullptr, 's'},
{"version", no_argument, nullptr, 'V'},
{"verbose", no_argument, nullptr, 'v'},
{"warning", required_argument, nullptr, 'W'},
{"max-errors", required_argument, nullptr, 'X'},
{"color", required_argument, &longOpt, 'c'},
{"MC", no_argument, &longOpt, 'C'},
{"MG", no_argument, &longOpt, 'G'},
{"MP", no_argument, &longOpt, 'P'},
{"MQ", required_argument, &longOpt, 'Q'},
{"MT", required_argument, &longOpt, 'T'},
{nullptr, no_argument, nullptr, 0 },
};
// clang-format off: nested initializers
static Usage usage = {
.name = "rgbasm",
.flags = {
"[-EhVvw]", "[-B depth]", "[-b chars]", "[-D name[=value]]", "[-g chars]", "[-I path]",
"[-M depend_file]", "[-MC]", "[-MG]", "[-MP]", "[-MT target_file]", "[-MQ target_file]",
"[-o out_file]", "[-P include_file]", "[-p pad_value]", "[-Q precision]", "[-r depth]",
"[-s features:state_file]", "[-W warning]", "[-X max_errors]", "<file>",
},
.options = {
{{"-E", "--export-all"}, {"export all labels"}},
{{"-M", "--dependfile <path>"}, {"set the output dependency file"}},
{{"-o", "--output <path>"}, {"set the output object file"}},
{{"-p", "--pad-value <value>"}, {"set the value to use for `DS`"}},
{{"-s", "--state <features>:<path>"}, {"set an output state file"}},
{{"-V", "--version"}, {"print RGBASM version and exit"}},
{{"-W", "--warning <warning>"}, {"enable or disable warnings"}},
},
};
// clang-format on
static std::string escapeMakeChars(std::string &str) {
std::string escaped;
size_t pos = 0;
for (;;) {
@@ -46,65 +126,6 @@ static std::string make_escape(std::string &str) {
return escaped;
}
// Short options
static char const *optstring = "b:D:Eg:hI:M:o:P:p:Q:r:s:VvW:wX:";
// Variables for the long-only options
static int depType; // Variants of `-M`
// Equivalent long options
// Please keep in the same order as short opts.
// Also, make sure long opts don't create ambiguity:
// A long opt's name should start with the same letter as its short opt,
// except if it doesn't create any ambiguity (`verbose` versus `version`).
// This is because long opt matching, even to a single char, is prioritized
// over short opt matching.
static option const longopts[] = {
{"binary-digits", required_argument, nullptr, 'b'},
{"define", required_argument, nullptr, 'D'},
{"export-all", no_argument, nullptr, 'E'},
{"gfx-chars", required_argument, nullptr, 'g'},
{"help", no_argument, nullptr, 'h'},
{"include", required_argument, nullptr, 'I'},
{"dependfile", required_argument, nullptr, 'M'},
{"MC", no_argument, &depType, 'C'},
{"MG", no_argument, &depType, 'G'},
{"MP", no_argument, &depType, 'P'},
{"MQ", required_argument, &depType, 'Q'},
{"MT", required_argument, &depType, 'T'},
{"output", required_argument, nullptr, 'o'},
{"preinclude", required_argument, nullptr, 'P'},
{"pad-value", required_argument, nullptr, 'p'},
{"q-precision", required_argument, nullptr, 'Q'},
{"recursion-depth", required_argument, nullptr, 'r'},
{"state", required_argument, nullptr, 's'},
{"version", no_argument, nullptr, 'V'},
{"verbose", no_argument, nullptr, 'v'},
{"warning", required_argument, nullptr, 'W'},
{"max-errors", required_argument, nullptr, 'X'},
{nullptr, no_argument, nullptr, 0 }
};
// clang-format off: long string literal
static Usage usage(
"Usage: rgbasm [-EhVvw] [-b chars] [-D name[=value]] [-g chars] [-I path]\n"
" [-M depend_file] [-MC] [-MG] [-MP] [-MT target_file] [-MQ target_file]\n"
" [-o out_file] [-P include_file] [-p pad_value] [-Q precision]\n"
" [-r depth] [-s features:state_file] [-W warning] [-X max_errors]\n"
" <file>\n"
"Useful options:\n"
" -E, --export-all export all labels\n"
" -M, --dependfile <path> set the output dependency file\n"
" -o, --output <path> set the output object file\n"
" -p, --pad-value <value> set the value to use for `ds'\n"
" -s, --state <features>:<path> set an output state file\n"
" -V, --version print RGBASM version and exit\n"
" -W, --warning <warning> enable or disable warnings\n"
"\n"
"For help, use `man rgbasm' or go to https://rgbds.gbdev.io/docs/\n"
);
// clang-format on
// Parse a comma-separated string of '-s/--state' features
static std::vector<StateFeature> parseStateFeatures(char *str) {
std::vector<StateFeature> features;
@@ -114,7 +135,7 @@ static std::vector<StateFeature> parseStateFeatures(char *str) {
if (next) {
*next++ = '\0';
}
// Trim whitespace from the beginning of `feature`...
// Trim blank spaces from the beginning of `feature`...
feature += strspn(feature, " \t");
// ...and from the end
if (char *end = strpbrk(feature, " \t"); end) {
@@ -122,7 +143,7 @@ static std::vector<StateFeature> parseStateFeatures(char *str) {
}
// A feature must be specified
if (*feature == '\0') {
fatal("Empty feature for option 's'");
fatal("Empty feature for option '-s'");
}
// Parse the `feature` and update the `features` list
static UpperMap<StateFeature> const featureNames{
@@ -134,14 +155,14 @@ static std::vector<StateFeature> parseStateFeatures(char *str) {
};
if (!strcasecmp(feature, "all")) {
if (!features.empty()) {
warnx("Redundant feature before \"%s\" for option 's'", feature);
warnx("Redundant feature before \"%s\" for option '-s'", feature);
}
features.assign({STATE_EQU, STATE_VAR, STATE_EQUS, STATE_CHAR, STATE_MACRO});
} else if (auto search = featureNames.find(feature); search == featureNames.end()) {
fatal("Invalid feature for option 's': \"%s\"", feature);
fatal("Invalid feature for option '-s': \"%s\"", feature);
} else if (StateFeature value = search->second;
std::find(RANGE(features), value) != features.end()) {
warnx("Ignoring duplicate feature for option 's': \"%s\"", feature);
warnx("Ignoring duplicate feature for option '-s': \"%s\"", feature);
} else {
features.push_back(value);
}
@@ -150,192 +171,165 @@ static std::vector<StateFeature> parseStateFeatures(char *str) {
return features;
}
int main(int argc, char *argv[]) {
// Support SOURCE_DATE_EPOCH for reproducible builds
// https://reproducible-builds.org/docs/source-date-epoch/
time_t now = time(nullptr);
if (char const *sourceDateEpoch = getenv("SOURCE_DATE_EPOCH"); sourceDateEpoch) {
now = static_cast<time_t>(strtoul(sourceDateEpoch, nullptr, 0));
}
sym_Init(now);
// Maximum of 100 errors only applies if rgbasm is printing errors to a terminal
if (isatty(STDERR_FILENO)) {
options.maxErrors = 100;
}
// Local options
char const *dependFileName = nullptr; // -M
std::unordered_map<std::string, std::vector<StateFeature>> stateFileSpecs; // -s
for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) {
static void parseArg(int ch, char *arg) {
switch (ch) {
char *endptr;
case 'B':
if (!trace_ParseTraceDepth(arg)) {
fatal("Invalid argument for option '-B'");
}
break;
case 'b':
if (strlen(musl_optarg) == 2) {
opt_B(musl_optarg);
if (strlen(arg) == 2) {
opt_B(arg);
} else {
fatal("Must specify exactly 2 characters for option 'b'");
fatal("Must specify exactly 2 characters for option '-b'");
}
break;
char *equals;
case 'D':
equals = strchr(musl_optarg, '=');
case 'D': {
char *equals = strchr(arg, '=');
if (equals) {
*equals = '\0';
sym_AddString(musl_optarg, std::make_shared<std::string>(equals + 1));
sym_AddString(arg, std::make_shared<std::string>(equals + 1));
} else {
sym_AddString(musl_optarg, std::make_shared<std::string>("1"));
sym_AddString(arg, std::make_shared<std::string>("1"));
}
break;
}
case 'E':
sym_SetExportAll(true);
options.exportAll = true;
break;
case 'g':
if (strlen(musl_optarg) == 4) {
opt_G(musl_optarg);
if (strlen(arg) == 4) {
opt_G(arg);
} else {
fatal("Must specify exactly 4 characters for option 'g'");
fatal("Must specify exactly 4 characters for option '-g'");
}
break;
// LCOV_EXCL_START
case 'h':
usage.printAndExit(0); // LCOV_EXCL_LINE
usage.printAndExit(0);
// LCOV_EXCL_STOP
case 'I':
fstk_AddIncludePath(musl_optarg);
fstk_AddIncludePath(arg);
break;
case 'M':
if (options.dependFile) {
warnx("Overriding dependfile %s", dependFileName);
}
if (strcmp("-", musl_optarg)) {
options.dependFile = fopen(musl_optarg, "w");
dependFileName = musl_optarg;
} else {
options.dependFile = stdout;
dependFileName = "<stdout>";
}
if (options.dependFile == nullptr) {
// LCOV_EXCL_START
fatal("Failed to open dependfile \"%s\": %s", dependFileName, strerror(errno));
// LCOV_EXCL_STOP
if (localOptions.dependFileName) {
warnx(
"Overriding dependency file \"%s\"",
*localOptions.dependFileName == "-" ? "<stdout>"
: localOptions.dependFileName->c_str()
);
}
localOptions.dependFileName = arg;
break;
case 'o':
if (!options.objectFileName.empty()) {
warnx("Overriding output filename %s", options.objectFileName.c_str());
if (options.objectFileName) {
warnx("Overriding output file \"%s\"", options.objectFileName->c_str());
}
options.objectFileName = musl_optarg;
verbosePrint("Output filename %s\n", options.objectFileName.c_str()); // LCOV_EXCL_LINE
options.objectFileName = arg;
break;
case 'P':
fstk_AddPreIncludeFile(musl_optarg);
fstk_AddPreIncludeFile(arg);
break;
unsigned long padByte;
case 'p':
padByte = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'p'");
if (std::optional<uint64_t> padByte = parseWholeNumber(arg); !padByte) {
fatal("Invalid argument for option '-p'");
} else if (*padByte > 0xFF) {
fatal("Argument for option '-p' must be between 0 and 0xFF");
} else {
opt_P(*padByte);
}
if (padByte > 0xFF) {
fatal("Argument for option 'p' must be between 0 and 0xFF");
}
opt_P(padByte);
break;
case 'Q': {
char const *precisionArg = musl_optarg;
char const *precisionArg = arg;
if (precisionArg[0] == '.') {
++precisionArg;
}
unsigned long precision = strtoul(precisionArg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'Q'");
if (std::optional<uint64_t> precision = parseWholeNumber(precisionArg); !precision) {
fatal("Invalid argument for option '-Q'");
} else if (*precision < 1 || *precision > 31) {
fatal("Argument for option '-Q' must be between 1 and 31");
} else {
opt_Q(*precision);
}
if (precision < 1 || precision > 31) {
fatal("Argument for option 'Q' must be between 1 and 31");
}
opt_Q(precision);
break;
}
case 'r':
options.maxRecursionDepth = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'r'");
if (std::optional<uint64_t> maxDepth = parseWholeNumber(arg); !maxDepth) {
fatal("Invalid argument for option '-r'");
} else if (errno == ERANGE) {
fatal("Argument for option '-r' is out of range");
} else {
options.maxRecursionDepth = *maxDepth;
}
break;
case 's': {
// Split "<features>:<name>" so `musl_optarg` is "<features>" and `name` is "<name>"
char *name = strchr(musl_optarg, ':');
// Split "<features>:<name>" so `arg` is "<features>" and `name` is "<name>"
char *name = strchr(arg, ':');
if (!name) {
fatal("Invalid argument for option 's'");
fatal("Invalid argument for option '-s'");
}
*name++ = '\0';
std::vector<StateFeature> features = parseStateFeatures(musl_optarg);
std::vector<StateFeature> features = parseStateFeatures(arg);
if (stateFileSpecs.find(name) != stateFileSpecs.end()) {
warnx("Overriding state filename %s", name);
if (localOptions.stateFileSpecs.find(name) != localOptions.stateFileSpecs.end()) {
warnx("Overriding state file \"%s\"", name);
}
verbosePrint("State filename %s\n", name); // LCOV_EXCL_LINE
stateFileSpecs.emplace(name, std::move(features));
localOptions.stateFileSpecs.emplace(name, std::move(features));
break;
}
// LCOV_EXCL_START
case 'V':
printf("rgbasm %s\n", get_package_version_string());
exit(0);
case 'v':
// LCOV_EXCL_START
options.verbose = true;
incrementVerbosity();
break;
// LCOV_EXCL_STOP
case 'W':
opt_W(musl_optarg);
opt_W(arg);
break;
case 'w':
warnings.state.warningsEnabled = false;
break;
case 'X': {
uint64_t maxErrors = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'X'");
case 'X':
if (std::optional<uint64_t> maxErrors = parseWholeNumber(arg); !maxErrors) {
fatal("Invalid argument for option '-X'");
} else if (*maxErrors > UINT64_MAX) {
fatal("Argument for option '-X' must be between 0 and %" PRIu64, UINT64_MAX);
} else {
options.maxErrors = *maxErrors;
}
break;
case 0: // Long-only options
switch (longOpt) {
case 'c':
if (!style_Parse(arg)) {
fatal("Invalid argument for option '--color'");
}
if (maxErrors > UINT64_MAX) {
fatal("Argument for option 'X' must be between 0 and %" PRIu64, UINT64_MAX);
}
options.maxErrors = maxErrors;
break;
}
// Long-only options
case 0:
switch (depType) {
case 'C':
options.missingIncludeState = GEN_CONTINUE;
break;
@@ -350,58 +344,236 @@ int main(int argc, char *argv[]) {
case 'Q':
case 'T': {
std::string newTarget = musl_optarg;
if (depType == 'Q') {
newTarget = make_escape(newTarget);
std::string newTarget = arg;
if (longOpt == 'Q') {
newTarget = escapeMakeChars(newTarget);
}
if (!options.targetFileName.empty()) {
options.targetFileName += ' ';
if (options.targetFileName) {
*options.targetFileName += ' ';
*options.targetFileName += newTarget;
} else {
options.targetFileName = newTarget;
}
options.targetFileName += newTarget;
break;
}
}
break;
// Unrecognized options
case 1: // Positional argument
if (localOptions.inputFileName) {
usage.printAndExit("More than one input file specified");
}
localOptions.inputFileName = arg;
break;
// LCOV_EXCL_START
default:
usage.printAndExit(1); // LCOV_EXCL_LINE
usage.printAndExit(1);
// LCOV_EXCL_STOP
}
}
if (options.targetFileName.empty() && !options.objectFileName.empty()) {
// LCOV_EXCL_START
static void verboseOutputConfig() {
if (!checkVerbosity(VERB_CONFIG)) {
return;
}
style_Set(stderr, STYLE_MAGENTA, false);
fprintf(stderr, "rgbasm %s\n", get_package_version_string());
printVVVVVVerbosity();
fputs("Options:\n", stderr);
// -E/--export-all
if (options.exportAll) {
fputs("\tExport all labels by default\n", stderr);
}
// -b/--binary-digits
if (options.binDigits[0] != '0' || options.binDigits[1] != '1') {
fprintf(
stderr, "\tBinary digits: '%c', '%c'\n", options.binDigits[0], options.binDigits[1]
);
}
// -g/--gfx-chars
if (options.gfxDigits[0] != '0' || options.gfxDigits[1] != '1' || options.gfxDigits[2] != '2'
|| options.gfxDigits[3] != '3') {
fprintf(
stderr,
"\tGraphics characters: '%c', '%c', '%c', '%c'\n",
options.gfxDigits[0],
options.gfxDigits[1],
options.gfxDigits[2],
options.gfxDigits[3]
);
}
// -Q/--q-precision
fprintf(
stderr,
"\tFixed-point precision: Q%d.%" PRIu8 "\n",
32 - options.fixPrecision,
options.fixPrecision
);
// -p/--pad-value
fprintf(stderr, "\tPad value: 0x%02" PRIx8 "\n", options.padByte);
// -r/--recursion-depth
fprintf(stderr, "\tMaximum recursion depth %zu\n", options.maxRecursionDepth);
// -X/--max-errors
if (options.maxErrors) {
fprintf(stderr, "\tMaximum %" PRIu64 " errors\n", options.maxErrors);
}
// -D/--define
static bool hasDefines = false; // `static` so `sym_ForEach` callback can see it
sym_ForEach([](Symbol &sym) {
if (!sym.isBuiltin && sym.type == SYM_EQUS) {
if (!hasDefines) {
fputs("\tDefinitions:\n", stderr);
hasDefines = true;
}
fprintf(stderr, "\t - def %s equs \"%s\"\n", sym.name.c_str(), sym.getEqus()->c_str());
}
});
// -s/--state
if (!localOptions.stateFileSpecs.empty()) {
fputs("\tOutput state files:\n", stderr);
static char const *featureNames[NB_STATE_FEATURES] = {
"equ",
"var",
"equs",
"char",
"macro",
};
for (auto const &[name, features] : localOptions.stateFileSpecs) {
fprintf(stderr, "\t - %s: ", name == "-" ? "<stdout>" : name.c_str());
for (size_t i = 0; i < features.size(); ++i) {
if (i > 0) {
fputs(", ", stderr);
}
fputs(featureNames[features[i]], stderr);
}
putc('\n', stderr);
}
}
// asmfile
if (localOptions.inputFileName) {
fprintf(
stderr,
"\tInput asm file: %s\n",
*localOptions.inputFileName == "-" ? "<stdin>" : localOptions.inputFileName->c_str()
);
}
// -o/--output
if (options.objectFileName) {
fprintf(stderr, "\tOutput object file: %s\n", options.objectFileName->c_str());
}
fstk_VerboseOutputConfig();
if (localOptions.dependFileName) {
fprintf(stderr, "\tOutput dependency file: %s\n", localOptions.dependFileName->c_str());
// -MT or -MQ
if (options.targetFileName) {
fprintf(stderr, "\tTarget file(s): %s\n", options.targetFileName->c_str());
}
// -MG or -MC
switch (options.missingIncludeState) {
case INC_ERROR:
fputs("\tExit with an error on a missing dependency\n", stderr);
break;
case GEN_EXIT:
fputs("\tExit normally on a missing dependency\n", stderr);
break;
case GEN_CONTINUE:
fputs("\tContinue processing after a missing dependency\n", stderr);
break;
}
// -MP
if (options.generatePhonyDeps) {
fputs("\tGenerate phony dependencies\n", stderr);
}
}
fputs("Ready.\n", stderr);
style_Reset(stderr);
}
// LCOV_EXCL_STOP
int main(int argc, char *argv[]) {
// Support SOURCE_DATE_EPOCH for reproducible builds
// https://reproducible-builds.org/docs/source-date-epoch/
time_t now = time(nullptr);
if (char const *sourceDateEpoch = getenv("SOURCE_DATE_EPOCH"); sourceDateEpoch) {
// Use `strtoul`, not `parseWholeNumber`, because SOURCE_DATE_EPOCH does
// not conventionally support our custom base prefixes
now = static_cast<time_t>(strtoul(sourceDateEpoch, nullptr, 0));
}
sym_Init(now);
// Maximum of 100 errors only applies if rgbasm is printing errors to a terminal
if (isatty(STDERR_FILENO)) {
options.maxErrors = 100; // LCOV_EXCL_LINE
}
cli_ParseArgs(argc, argv, optstring, longopts, parseArg, usage);
if (!options.targetFileName && options.objectFileName) {
options.targetFileName = options.objectFileName;
}
if (argc == musl_optind) {
usage.printAndExit("Please specify an input file (pass `-` to read from standard input)");
} else if (argc != musl_optind + 1) {
usage.printAndExit("More than one input file specified");
verboseOutputConfig();
if (!localOptions.inputFileName) {
usage.printAndExit("No input file specified (pass \"-\" to read from standard input)");
}
std::string mainFileName = argv[musl_optind];
// LCOV_EXCL_START
verbosePrint(
VERB_NOTICE,
"Assembling \"%s\"\n",
*localOptions.inputFileName == "-" ? "<stdin>" : localOptions.inputFileName->c_str()
);
// LCOV_EXCL_STOP
verbosePrint("Assembling %s\n", mainFileName.c_str()); // LCOV_EXCL_LINE
if (options.dependFile && options.targetFileName.empty()) {
if (localOptions.dependFileName) {
if (!options.targetFileName) {
fatal("Dependency files can only be created if a target file is specified with either "
"-o, -MQ or -MT");
"'-o', '-MQ' or '-MT'");
}
options.printDep(mainFileName);
if (*localOptions.dependFileName == "-") {
options.dependFile = stdout;
} else {
options.dependFile = fopen(localOptions.dependFileName->c_str(), "w");
if (options.dependFile == nullptr) {
// LCOV_EXCL_START
fatal(
"Failed to open dependency file \"%s\": %s",
localOptions.dependFileName->c_str(),
strerror(errno)
);
// LCOV_EXCL_STOP
}
}
}
options.printDep(*localOptions.inputFileName);
charmap_New(DEFAULT_CHARMAP_NAME, nullptr);
// Init lexer and file stack, providing file info
fstk_Init(mainFileName);
fstk_Init(*localOptions.inputFileName);
// Perform parse (`yy::parser` is auto-generated from `parser.y`)
if (yy::parser parser; parser.parse() != 0) {
if (warnings.nbErrors == 0) {
warnings.nbErrors = 1;
}
// Exited due to YYABORT or YYNOMEM
fatal("Unrecoverable error while parsing"); // LCOV_EXCL_LINE
}
// If parse aborted without errors due to a missing INCLUDE, and `-MG` was given, exit normally
if (fstk_FailedOnMissingInclude()) {
requireZeroErrors();
return 0;
}
if (!fstk_FailedOnMissingInclude()) {
sect_CheckUnionClosed();
sect_CheckLoadClosed();
sect_CheckSizes();
@@ -409,18 +581,12 @@ int main(int argc, char *argv[]) {
charmap_CheckStack();
opt_CheckStack();
sect_CheckStack();
}
requireZeroErrors();
// If parse aborted due to missing an include, and `-MG` was given, exit normally
if (fstk_FailedOnMissingInclude()) {
return 0;
}
out_WriteObject();
for (auto [name, features] : stateFileSpecs) {
for (auto const &[name, features] : localOptions.stateFileSpecs) {
out_WriteState(name, features);
}

View File

@@ -1,19 +1,20 @@
// SPDX-License-Identifier: MIT
#include <ctype.h>
#include <errno.h>
#include <iterator> // std::size
#include <optional>
#include <stack>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "helpers.hpp" // assume
#include "diagnostics.hpp"
#include "util.hpp"
#include "asm/fixpoint.hpp"
#include "asm/fstack.hpp"
#include "asm/lexer.hpp"
#include "asm/main.hpp" // options
#include "asm/section.hpp"
#include "asm/warning.hpp"
struct OptStackEntry {
@@ -49,104 +50,80 @@ void opt_R(size_t maxRecursionDepth) {
}
void opt_W(char const *flag) {
if (warnings.processWarningFlag(flag) == "numeric-string") {
warning(WARNING_OBSOLETE, "Warning flag \"numeric-string\" is deprecated");
}
warnings.processWarningFlag(flag);
}
void opt_Parse(char const *s) {
switch (s[0]) {
if (s[0] == '-') {
++s; // Skip a leading '-'
}
char c = *s++;
while (isBlankSpace(*s)) {
++s; // Skip leading blank spaces
}
switch (c) {
case 'b':
if (strlen(&s[1]) == 2) {
opt_B(&s[1]);
if (strlen(s) == 2) {
opt_B(s);
} else {
error("Must specify exactly 2 characters for option 'b'");
}
break;
case 'g':
if (strlen(&s[1]) == 4) {
opt_G(&s[1]);
if (strlen(s) == 4) {
opt_G(s);
} else {
error("Must specify exactly 4 characters for option 'g'");
}
break;
case 'p':
if (strlen(&s[1]) <= 2) {
int result;
unsigned int padByte;
result = sscanf(&s[1], "%x", &padByte);
if (result != 1) {
if (std::optional<uint64_t> padByte = parseWholeNumber(s); !padByte) {
error("Invalid argument for option 'p'");
} else if (*padByte > 0xFF) {
error("Argument for option 'p' must be between 0 and 0xFF");
} else {
// Two characters cannot be scanned as a hex number greater than 0xFF
assume(padByte <= 0xFF);
opt_P(padByte);
}
} else {
error("Invalid argument for option 'p'");
opt_P(*padByte);
}
break;
char const *precisionArg;
case 'Q':
precisionArg = &s[1];
if (precisionArg[0] == '.') {
++precisionArg;
if (s[0] == '.') {
++s; // Skip leading '.'
}
if (strlen(precisionArg) <= 2) {
int result;
unsigned int fixPrecision;
result = sscanf(precisionArg, "%u", &fixPrecision);
if (result != 1) {
if (std::optional<uint64_t> precision = parseWholeNumber(s); !precision) {
error("Invalid argument for option 'Q'");
} else if (fixPrecision < 1 || fixPrecision > 31) {
} else if (*precision < 1 || *precision > 31) {
error("Argument for option 'Q' must be between 1 and 31");
} else {
opt_Q(fixPrecision);
}
} else {
error("Invalid argument for option 'Q'");
opt_Q(*precision);
}
break;
case 'r': {
++s; // Skip 'r'
while (isblank(*s)) {
++s; // Skip leading whitespace
}
if (s[0] == '\0') {
error("Missing argument to option 'r'");
break;
}
char *endptr;
unsigned long maxRecursionDepth = strtoul(s, &endptr, 10);
if (*endptr != '\0') {
error("Invalid argument to option 'r' (\"%s\")", s);
case 'r':
if (std::optional<uint64_t> maxRecursionDepth = parseWholeNumber(s); !maxRecursionDepth) {
error("Invalid argument for option 'r'");
} else if (errno == ERANGE) {
error("Argument to 'r' is out of range (\"%s\")", s);
error("Argument for option 'r' is out of range");
} else {
opt_R(maxRecursionDepth);
opt_R(*maxRecursionDepth);
}
break;
}
case 'W':
if (strlen(&s[1]) > 0) {
opt_W(&s[1]);
if (strlen(s) > 0) {
opt_W(s);
} else {
error("Must specify an argument for option 'W'");
}
break;
default:
error("Unknown option '%c'", s[0]);
error("Unknown option '%c'", c);
break;
}
}

View File

@@ -6,13 +6,16 @@
#include <deque>
#include <errno.h>
#include <inttypes.h>
#include <memory>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>
#include "diagnostics.hpp"
#include "helpers.hpp" // assume, Defer
#include "linkdefs.hpp"
#include "platform.hpp"
#include "asm/charmap.hpp"
@@ -83,17 +86,17 @@ static void writeSection(Section const &sect, FILE *file) {
putLong(sect.size, file);
assume((sect.type & SECTTYPE_TYPE_MASK) == sect.type);
bool isUnion = sect.modifier == SECTION_UNION;
bool isFragment = sect.modifier == SECTION_FRAGMENT;
putc(sect.type | isUnion << 7 | isFragment << 6, file);
putc(sect.type | isUnion << SECTTYPE_UNION_BIT | isFragment << SECTTYPE_FRAGMENT_BIT, file);
putLong(sect.org, file);
putLong(sect.bank, file);
putc(sect.align, file);
putLong(sect.alignOfs, file);
if (sect_HasData(sect.type)) {
if (sectTypeHasData(sect.type)) {
fwrite(sect.data.data(), 1, sect.size, file);
putLong(sect.patches.size(), file);
@@ -120,7 +123,7 @@ static void writeSymbol(Symbol const &sym, FILE *file) {
}
}
static void registerUnregisteredSymbol(Symbol &sym) {
void out_RegisterSymbol(Symbol &sym) {
// Check for `sym.src`, to skip any built-in symbol from rgbasm
if (sym.src && sym.ID == UINT32_MAX && !sym_IsPC(&sym)) {
sym.ID = objectSymbols.size(); // Set the symbol's ID within the object file
@@ -129,95 +132,6 @@ static void registerUnregisteredSymbol(Symbol &sym) {
}
}
static void writeRpn(std::vector<uint8_t> &rpnexpr, std::vector<uint8_t> const &rpn) {
size_t rpnptr = 0;
for (size_t offset = 0; offset < rpn.size();) {
uint8_t rpndata = rpn[offset++];
auto getSymName = [&]() {
std::string symName;
for (uint8_t c; (c = rpn[offset++]) != 0;) {
symName += c;
}
return symName;
};
switch (rpndata) {
Symbol *sym;
uint32_t value;
uint8_t b;
case RPN_CONST:
rpnexpr[rpnptr++] = RPN_CONST;
rpnexpr[rpnptr++] = rpn[offset++];
rpnexpr[rpnptr++] = rpn[offset++];
rpnexpr[rpnptr++] = rpn[offset++];
rpnexpr[rpnptr++] = rpn[offset++];
break;
case RPN_SYM:
// The symbol name is always written expanded
sym = sym_FindExactSymbol(getSymName());
if (sym->isConstant()) {
rpnexpr[rpnptr++] = RPN_CONST;
value = sym->getConstantValue();
} else {
rpnexpr[rpnptr++] = RPN_SYM;
registerUnregisteredSymbol(*sym); // Ensure that `sym->ID` is set
value = sym->ID;
}
rpnexpr[rpnptr++] = value & 0xFF;
rpnexpr[rpnptr++] = value >> 8;
rpnexpr[rpnptr++] = value >> 16;
rpnexpr[rpnptr++] = value >> 24;
break;
case RPN_BANK_SYM:
// The symbol name is always written expanded
sym = sym_FindExactSymbol(getSymName());
registerUnregisteredSymbol(*sym); // Ensure that `sym->ID` is set
value = sym->ID;
rpnexpr[rpnptr++] = RPN_BANK_SYM;
rpnexpr[rpnptr++] = value & 0xFF;
rpnexpr[rpnptr++] = value >> 8;
rpnexpr[rpnptr++] = value >> 16;
rpnexpr[rpnptr++] = value >> 24;
break;
case RPN_BANK_SECT:
rpnexpr[rpnptr++] = RPN_BANK_SECT;
do {
b = rpn[offset++];
rpnexpr[rpnptr++] = b;
} while (b != 0);
break;
case RPN_SIZEOF_SECT:
rpnexpr[rpnptr++] = RPN_SIZEOF_SECT;
do {
b = rpn[offset++];
rpnexpr[rpnptr++] = b;
} while (b != 0);
break;
case RPN_STARTOF_SECT:
rpnexpr[rpnptr++] = RPN_STARTOF_SECT;
do {
b = rpn[offset++];
rpnexpr[rpnptr++] = b;
} while (b != 0);
break;
default:
rpnexpr[rpnptr++] = rpndata;
break;
}
}
}
static void initPatch(Patch &patch, uint32_t type, Expression const &expr, uint32_t ofs) {
patch.type = type;
patch.src = fstk_GetFileStack();
@@ -227,20 +141,7 @@ static void initPatch(Patch &patch, uint32_t type, Expression const &expr, uint3
patch.offset = ofs;
patch.pcSection = sect_GetSymbolSection();
patch.pcOffset = sect_GetSymbolOffset();
if (expr.isKnown()) {
// If the RPN expr's value is known, output a constant directly
uint32_t val = expr.value();
patch.rpn.resize(5);
patch.rpn[0] = RPN_CONST;
patch.rpn[1] = val & 0xFF;
patch.rpn[2] = val >> 8;
patch.rpn[3] = val >> 16;
patch.rpn[4] = val >> 24;
} else {
patch.rpn.resize(expr.rpnPatchSize);
writeRpn(patch.rpn, expr.rpn);
}
expr.encode(patch.rpn);
}
void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32_t pcShift) {
@@ -273,7 +174,9 @@ static void writeAssert(Assertion const &assert, FILE *file) {
static void writeFileStackNode(FileStackNode const &node, FILE *file) {
putLong(node.parent ? node.parent->ID : UINT32_MAX, file);
putLong(node.lineNo, file);
putc(node.type, file);
putc(node.type | node.isQuiet << FSTACKNODE_QUIET_BIT, file);
if (node.type != NODE_REPT) {
putString(node.name(), file);
} else {
@@ -281,36 +184,35 @@ static void writeFileStackNode(FileStackNode const &node, FILE *file) {
putLong(nodeIters.size(), file);
// Iters are stored by decreasing depth, so reverse the order for output
for (uint32_t i = nodeIters.size(); i--;) {
putLong(nodeIters[i], file);
for (uint32_t iter : reversed(nodeIters)) {
putLong(iter, file);
}
}
}
void out_WriteObject() {
if (options.objectFileName.empty()) {
if (!options.objectFileName) {
return;
}
static FILE *file; // `static` so `sect_ForEach` callback can see it
if (options.objectFileName != "-") {
file = fopen(options.objectFileName.c_str(), "wb");
char const *objectFileName = options.objectFileName->c_str();
if (*options.objectFileName != "-") {
file = fopen(objectFileName, "wb");
} else {
options.objectFileName = "<stdout>";
objectFileName = "<stdout>";
(void)setmode(STDOUT_FILENO, O_BINARY);
file = stdout;
}
if (!file) {
// LCOV_EXCL_START
fatal(
"Failed to open object file '%s': %s", options.objectFileName.c_str(), strerror(errno)
);
fatal("Failed to open object file \"%s\": %s", objectFileName, strerror(errno));
// LCOV_EXCL_STOP
}
Defer closeFile{[&] { fclose(file); }};
// Also write symbols that weren't written above
sym_ForEach(registerUnregisteredSymbol);
sym_ForEach(out_RegisterSymbol);
fputs(RGBDS_OBJECT_VERSION_STRING, file);
putLong(RGBDS_OBJECT_REV, file);
@@ -320,12 +222,10 @@ void out_WriteObject() {
putLong(fileStackNodes.size(), file);
for (auto it = fileStackNodes.begin(); it != fileStackNodes.end(); ++it) {
FileStackNode const &node = **it;
writeFileStackNode(node, file);
writeFileStackNode(**it, file);
// The list is supposed to have decrementing IDs
assume(it + 1 == fileStackNodes.end() || it[1]->ID == node.ID - 1);
assume(it + 1 == fileStackNodes.end() || it[1]->ID == it[0]->ID - 1);
}
for (Symbol const *sym : objectSymbols) {
@@ -485,7 +385,7 @@ void out_WriteState(std::string name, std::vector<StateFeature> const &features)
}
if (!file) {
// LCOV_EXCL_START
fatal("Failed to open state file '%s': %s", name.c_str(), strerror(errno));
fatal("Failed to open state file \"%s\": %s", name.c_str(), strerror(errno));
// LCOV_EXCL_STOP
}
Defer closeFile{[&] { fclose(file); }};

View File

@@ -12,16 +12,12 @@
#include "linkdefs.hpp"
#include "asm/actions.hpp"
#include "asm/lexer.hpp"
#include "asm/macro.hpp"
#include "asm/rpn.hpp"
#include "asm/section.hpp"
struct AlignmentSpec {
uint8_t alignment;
uint16_t alignOfs;
};
struct ForArgs {
int32_t start;
int32_t stop;
@@ -36,7 +32,6 @@
%code {
#include <algorithm>
#include <ctype.h>
#include <inttypes.h>
#include <optional>
#include <stdio.h>
@@ -46,8 +41,8 @@
#include "extern/utf8decoder.hpp"
#include "helpers.hpp"
#include "util.hpp" // toLower, toUpper
#include "asm/actions.hpp"
#include "asm/charmap.hpp"
#include "asm/fixpoint.hpp"
#include "asm/fstack.hpp"
@@ -62,8 +57,10 @@
yy::parser::symbol_type yylex(); // Provided by lexer.cpp
template <typename N, typename S>
static auto handleSymbolByType(std::string const &symName, N numCallback, S strCallback) {
template<typename NumCallbackFnT, typename StrCallbackFnT>
static auto handleSymbolByType(
std::string const &symName, NumCallbackFnT numCallback, StrCallbackFnT strCallback
) {
if (Symbol *sym = sym_FindScopedSymbol(symName); sym && sym->type == SYM_EQUS) {
return strCallback(*sym->getEqus());
} else {
@@ -103,6 +100,7 @@
%token LBRACK "[" RBRACK "]"
%token LBRACKS "[[" RBRACKS "]]"
%token LPAREN "(" RPAREN ")"
%token QUESTIONMARK "?"
// Arithmetic operators
%token OP_ADD "+" OP_SUB "-"
@@ -111,6 +109,7 @@
// String operators
%token OP_CAT "++"
%token OP_STREQU "===" OP_STRNE "!=="
// Comparison operators
%token OP_LOGICEQU "==" OP_LOGICNE "!="
@@ -327,6 +326,7 @@
%token <std::string> LABEL "label"
%token <std::string> LOCAL "local label"
%token <std::string> ANON "anonymous label"
%token <std::string> QMACRO "quiet macro"
/******************** Data types ********************/
@@ -403,6 +403,7 @@
%type <SectionType> sect_type
%type <StrFmtArgList> strfmt_args
%type <StrFmtArgList> strfmt_va_args
%type <bool> maybe_quiet
%%
@@ -428,12 +429,14 @@ diff_mark:
%empty // OK
| OP_ADD {
::error(
"syntax error, unexpected + at the beginning of the line (is it a leftover diff mark?)"
"syntax error, unexpected '+' at the beginning of the line (is it a leftover diff "
"mark?)"
);
}
| OP_SUB {
::error(
"syntax error, unexpected - at the beginning of the line (is it a leftover diff mark?)"
"syntax error, unexpected '-' at the beginning of the line (is it a leftover diff "
"mark?)"
);
}
;
@@ -465,48 +468,19 @@ line_directive:
if:
POP_IF iconst NEWLINE {
lexer_IncIFDepth();
if ($2) {
lexer_RunIFBlock();
} else {
lexer_SetMode(LEXER_SKIP_TO_ELIF);
}
act_If($2);
}
;
elif:
POP_ELIF iconst NEWLINE {
if (lexer_GetIFDepth() == 0) {
fatal("Found ELIF outside of an IF construct");
}
if (lexer_RanIFBlock()) {
if (lexer_ReachedELSEBlock()) {
fatal("Found ELIF after an ELSE block");
}
lexer_SetMode(LEXER_SKIP_TO_ENDC);
} else if ($2) {
lexer_RunIFBlock();
} else {
lexer_SetMode(LEXER_SKIP_TO_ELIF);
}
act_Elif($2);
}
;
else:
POP_ELSE NEWLINE {
if (lexer_GetIFDepth() == 0) {
fatal("Found ELSE outside of an IF construct");
}
if (lexer_RanIFBlock()) {
if (lexer_ReachedELSEBlock()) {
fatal("Found ELSE after an ELSE block");
}
lexer_SetMode(LEXER_SKIP_TO_ENDC);
} else {
lexer_RunIFBlock();
lexer_ReachELSEBlock();
}
act_Else();
}
;
@@ -514,14 +488,14 @@ else:
plain_directive:
label
| label cpu_commands
| label data
| label macro
| label directive
;
endc:
POP_ENDC {
lexer_DecIFDepth();
act_Endc();
}
;
@@ -576,7 +550,13 @@ macro:
// Parsing 'macro_args' will restore the lexer's normal mode
lexer_SetMode(LEXER_RAW);
} macro_args {
fstk_RunMacro($1, $3);
fstk_RunMacro($1, $3, false);
}
| QMACRO {
// Parsing 'macro_args' will restore the lexer's normal mode
lexer_SetMode(LEXER_RAW);
} macro_args {
fstk_RunMacro($1, $3, true);
}
;
@@ -596,10 +576,6 @@ directive:
| println
| export
| export_def
| db
| dw
| dl
| ds
| section
| rsreset
| rsset
@@ -684,29 +660,10 @@ align:
align_spec:
uconst {
if ($1 > 16) {
::error("Alignment must be between 0 and 16, not %u", $1);
$$.alignment = $$.alignOfs = 0;
} else {
$$.alignment = $1;
$$.alignOfs = 0;
}
$$ = act_Alignment($1, 0);
}
| uconst COMMA iconst {
if ($1 > 16) {
::error("Alignment must be between 0 and 16, not %u", $1);
$$.alignment = $$.alignOfs = 0;
} else if ($3 <= -(1 << $1) || $3 >= 1 << $1) {
::error(
"The absolute alignment offset (%" PRIu32 ") must be less than alignment size (%d)",
static_cast<uint32_t>($3 < 0 ? -$3 : $3),
1 << $1
);
$$.alignment = $$.alignOfs = 0;
} else {
$$.alignment = $1;
$$.alignOfs = $3 < 0 ? (1 << $1) + $3 : $3;
}
$$ = act_Alignment($1, $3);
}
;
@@ -796,28 +753,16 @@ assert_type:
assert:
POP_ASSERT assert_type relocexpr {
if (!$3.isKnown()) {
out_CreateAssert($2, $3, "", sect_GetOutputOffset());
} else if ($3.value() == 0) {
act_FailAssert($2);
}
act_Assert($2, $3, "");
}
| POP_ASSERT assert_type relocexpr COMMA string {
if (!$3.isKnown()) {
out_CreateAssert($2, $3, $5, sect_GetOutputOffset());
} else if ($3.value() == 0) {
act_FailAssertMsg($2, $5);
}
act_Assert($2, $3, $5);
}
| POP_STATIC_ASSERT assert_type iconst {
if ($3 == 0) {
act_FailAssert($2);
}
act_StaticAssert($2, $3, "");
}
| POP_STATIC_ASSERT assert_type iconst COMMA string {
if ($3 == 0) {
act_FailAssertMsg($2, $5);
}
act_StaticAssert($2, $3, $5);
}
;
@@ -847,10 +792,19 @@ load:
}
;
maybe_quiet:
%empty {
$$ = false;
}
| QUESTIONMARK {
$$ = true;
}
;
rept:
POP_REPT uconst NEWLINE capture_rept endofline {
if ($4.span.ptr) {
fstk_RunRept($2, $4.lineNo, $4.span);
POP_REPT maybe_quiet uconst NEWLINE capture_rept endofline {
if ($5.span.ptr) {
fstk_RunRept($3, $5.lineNo, $5.span, $2);
}
}
;
@@ -858,11 +812,11 @@ rept:
for:
POP_FOR {
lexer_ToggleStringExpansion(false);
} SYMBOL {
} maybe_quiet SYMBOL {
lexer_ToggleStringExpansion(true);
} COMMA for_args NEWLINE capture_rept endofline {
if ($8.span.ptr) {
fstk_RunFor($3, $6.start, $6.stop, $6.step, $8.lineNo, $8.span);
if ($9.span.ptr) {
fstk_RunFor($4, $7.start, $7.stop, $7.step, $9.lineNo, $9.span, $3);
}
}
;
@@ -902,11 +856,11 @@ break:
def_macro:
POP_MACRO {
lexer_ToggleStringExpansion(false);
} SYMBOL {
} maybe_quiet SYMBOL {
lexer_ToggleStringExpansion(true);
} NEWLINE capture_macro endofline {
if ($6.span.ptr) {
sym_AddMacro($3, $6.lineNo, $6.span);
if ($7.span.ptr) {
sym_AddMacro($4, $7.lineNo, $7.span, $3);
}
}
;
@@ -954,58 +908,6 @@ endu:
}
;
ds:
POP_DS uconst {
sect_Skip($2, true);
}
| POP_DS uconst COMMA ds_args trailing_comma {
sect_RelBytes($2, $4);
}
| POP_DS POP_ALIGN LBRACK align_spec RBRACK trailing_comma {
uint32_t n = sect_GetAlignBytes($4.alignment, $4.alignOfs);
sect_Skip(n, true);
sect_AlignPC($4.alignment, $4.alignOfs);
}
| POP_DS POP_ALIGN LBRACK align_spec RBRACK COMMA ds_args trailing_comma {
uint32_t n = sect_GetAlignBytes($4.alignment, $4.alignOfs);
sect_RelBytes(n, $7);
sect_AlignPC($4.alignment, $4.alignOfs);
}
;
ds_args:
reloc_8bit {
$$.push_back(std::move($1));
}
| ds_args COMMA reloc_8bit {
$$ = std::move($1);
$$.push_back(std::move($3));
}
;
db:
POP_DB {
sect_Skip(1, false);
}
| POP_DB constlist_8bit trailing_comma
;
dw:
POP_DW {
sect_Skip(2, false);
}
| POP_DW constlist_16bit trailing_comma
;
dl:
POP_DL {
sect_Skip(4, false);
}
| POP_DL constlist_32bit trailing_comma
;
def_equ:
def_id POP_EQU iconst {
$$ = std::move($1);
@@ -1121,8 +1023,8 @@ export_def:
;
include:
label POP_INCLUDE string endofline {
if (fstk_RunInclude($3)) {
label POP_INCLUDE maybe_quiet string endofline {
if (fstk_RunInclude($4, $3)) {
YYACCEPT;
}
}
@@ -1150,6 +1052,9 @@ charmap:
POP_CHARMAP string COMMA charmap_args trailing_comma {
charmap_Add($2, std::move($4));
}
| POP_CHARMAP CHARACTER COMMA charmap_args trailing_comma {
charmap_Add($2, std::move($4));
}
;
charmap_args:
@@ -1363,17 +1268,15 @@ relocexpr:
$$ = std::move($1);
}
| string_literal {
std::vector<int32_t> output = charmap_Convert($1);
$$.makeNumber(act_StringToNum(output));
$$.makeNumber(act_StringToNum($1));
}
| scoped_sym {
$$ = handleSymbolByType(
$1,
[](Expression const &expr) { return expr; },
[](std::string const &str) {
std::vector<int32_t> output = charmap_Convert(str);
Expression expr;
expr.makeNumber(act_StringToNum(output));
expr.makeNumber(act_StringToNum(str));
return expr;
}
);
@@ -1385,13 +1288,13 @@ relocexpr_no_str:
$$.makeNumber($1);
}
| CHARACTER {
std::vector<int32_t> output = charmap_Convert($1);
if (output.size() == 1) {
$$.makeNumber(static_cast<uint32_t>(output[0]));
} else {
::error("Character literals must be a single charmap unit");
$$.makeNumber(0);
$$.makeNumber(act_CharToNum($1));
}
| string OP_STREQU string {
$$.makeNumber($1.compare($3) == 0);
}
| string OP_STRNE string {
$$.makeNumber($1.compare($3) != 0);
}
| OP_LOGICNOT relocexpr %prec NEG {
$$.makeUnaryOp(RPN_LOGNOT, std::move($2));
@@ -1568,10 +1471,12 @@ relocexpr_no_str:
$$.makeNumber(pos != std::string::npos ? pos : -1);
}
| OP_STRIN LPAREN string COMMA string RPAREN {
warning(WARNING_OBSOLETE, "`STRIN` is deprecated; use 0-indexed `STRFIND` instead");
size_t pos = $3.find($5);
$$.makeNumber(pos != std::string::npos ? pos + 1 : 0);
}
| OP_STRRIN LPAREN string COMMA string RPAREN {
warning(WARNING_OBSOLETE, "`STRRIN` is deprecated; use 0-indexed `STRRFIND` instead");
size_t pos = $3.rfind($5);
$$.makeNumber(pos != std::string::npos ? pos + 1 : 0);
}
@@ -1598,36 +1503,13 @@ relocexpr_no_str:
$$.makeNumber(charSize);
}
| OP_CHARVAL LPAREN string COMMA iconst RPAREN {
if (size_t len = charmap_CharSize($3); len != 0) {
uint32_t idx = act_AdjustNegativeIndex($5, len, "CHARVAL");
if (std::optional<int32_t> val = charmap_CharValue($3, idx); val.has_value()) {
$$.makeNumber(*val);
} else {
warning(
WARNING_BUILTIN_ARG,
"CHARVAL: Index %" PRIu32 " is past the end of the character mapping",
idx
);
$$.makeNumber(0);
}
} else {
::error("CHARVAL: No character mapping for \"%s\"", $3.c_str());
$$.makeNumber(0);
$$.makeNumber(act_CharVal($3, $5));
}
| OP_CHARVAL LPAREN string RPAREN {
$$.makeNumber(act_CharVal($3));
}
| OP_STRBYTE LPAREN string COMMA iconst RPAREN {
size_t len = $3.length();
uint32_t idx = act_AdjustNegativeIndex($5, len, "STRBYTE");
if (idx < len) {
$$.makeNumber(static_cast<uint8_t>($3[idx]));
} else {
warning(
WARNING_BUILTIN_ARG,
"STRBYTE: Index %" PRIu32 " is past the end of the string",
idx
);
$$.makeNumber(0);
}
$$.makeNumber(act_StringByte($3, $5));
}
| LPAREN relocexpr RPAREN {
$$ = std::move($2);
@@ -1685,35 +1567,22 @@ string_literal:
}
}
| OP_STRSLICE LPAREN string COMMA iconst COMMA iconst RPAREN {
size_t len = act_StringLen($3, false);
uint32_t start = act_AdjustNegativeIndex($5, len, "STRSLICE");
uint32_t stop = act_AdjustNegativeIndex($7, len, "STRSLICE");
$$ = act_StringSlice($3, start, stop);
$$ = act_StringSlice($3, $5, $7);
}
| OP_STRSLICE LPAREN string COMMA iconst RPAREN {
size_t len = act_StringLen($3, false);
uint32_t start = act_AdjustNegativeIndex($5, len, "STRSLICE");
$$ = act_StringSlice($3, start, len);
$$ = act_StringSlice($3, $5, std::nullopt);
}
| OP_STRSUB LPAREN string COMMA iconst COMMA uconst RPAREN {
size_t len = act_StringLen($3, false);
uint32_t pos = act_AdjustNegativePos($5, len, "STRSUB");
$$ = act_StringSub($3, pos, $7);
$$ = act_StringSub($3, $5, $7);
}
| OP_STRSUB LPAREN string COMMA iconst RPAREN {
size_t len = act_StringLen($3, false);
uint32_t pos = act_AdjustNegativePos($5, len, "STRSUB");
$$ = act_StringSub($3, pos, pos > len ? 0 : len + 1 - pos);
$$ = act_StringSub($3, $5, std::nullopt);
}
| OP_STRCHAR LPAREN string COMMA iconst RPAREN {
size_t len = act_CharLen($3);
uint32_t idx = act_AdjustNegativeIndex($5, len, "STRCHAR");
$$ = act_StringChar($3, idx);
$$ = act_StringChar($3, $5);
}
| OP_CHARSUB LPAREN string COMMA iconst RPAREN {
size_t len = act_CharLen($3);
uint32_t pos = act_AdjustNegativePos($5, len, "CHARSUB");
$$ = act_CharSub($3, pos);
$$ = act_CharSub($3, $5);
}
| OP_REVCHAR LPAREN charmap_args RPAREN {
bool unique;
@@ -1732,11 +1601,11 @@ string_literal:
}
| OP_STRUPR LPAREN string RPAREN {
$$ = std::move($3);
std::transform(RANGE($$), $$.begin(), [](char c) { return toupper(c); });
std::transform(RANGE($$), $$.begin(), toUpper);
}
| OP_STRLWR LPAREN string RPAREN {
$$ = std::move($3);
std::transform(RANGE($$), $$.begin(), [](char c) { return tolower(c); });
std::transform(RANGE($$), $$.begin(), toLower);
}
| OP_STRRPL LPAREN string COMMA string COMMA string RPAREN {
$$ = act_StringReplace($3, $5, $7);
@@ -1745,23 +1614,7 @@ string_literal:
$$ = act_StringFormat($3.format, $3.args);
}
| POP_SECTION LPAREN scoped_sym RPAREN {
Symbol *sym = sym_FindScopedValidSymbol($3);
if (!sym) {
if (sym_IsPurgedScoped($3)) {
fatal("Unknown symbol \"%s\"; it was purged", $3.c_str());
} else {
fatal("Unknown symbol \"%s\"", $3.c_str());
}
}
Section const *section = sym->getSection();
if (!section) {
fatal("\"%s\" does not belong to any section", sym->name.c_str());
}
// Section names are capped by rgbasm's maximum string length,
// so this currently can't overflow.
$$ = section->name;
$$ = act_SectionName($3);
}
;
@@ -1773,7 +1626,7 @@ string:
if (Symbol *sym = sym_FindScopedSymbol($1); sym && sym->type == SYM_EQUS) {
$$ = *sym->getEqus();
} else {
::error("'%s' is not a string symbol", $1.c_str());
::error("`%s` is not a string symbol", $1.c_str());
}
}
;
@@ -1900,15 +1753,19 @@ sect_attrs:
}
;
// CPU commands.
// CPU instructions and data declarations
cpu_commands:
cpu_command
| cpu_command DOUBLE_COLON cpu_commands
data:
datum
| datum DOUBLE_COLON data
;
cpu_command:
sm83_adc
datum:
db
| dw
| dl
| ds
| sm83_adc
| sm83_add
| sm83_and
| sm83_bit
@@ -1956,6 +1813,56 @@ cpu_command:
| sm83_xor
;
ds:
POP_DS uconst {
sect_Skip($2, true);
}
| POP_DS uconst COMMA ds_args trailing_comma {
sect_RelBytes($2, $4);
}
| POP_DS POP_ALIGN LBRACK align_spec RBRACK trailing_comma {
uint32_t n = sect_GetAlignBytes($4.alignment, $4.alignOfs);
sect_Skip(n, true);
sect_AlignPC($4.alignment, $4.alignOfs);
}
| POP_DS POP_ALIGN LBRACK align_spec RBRACK COMMA ds_args trailing_comma {
uint32_t n = sect_GetAlignBytes($4.alignment, $4.alignOfs);
sect_RelBytes(n, $7);
sect_AlignPC($4.alignment, $4.alignOfs);
}
;
ds_args:
reloc_8bit {
$$.push_back(std::move($1));
}
| ds_args COMMA reloc_8bit {
$$ = std::move($1);
$$.push_back(std::move($3));
}
;
db:
POP_DB {
sect_Skip(1, false);
}
| POP_DB constlist_8bit trailing_comma
;
dw:
POP_DW {
sect_Skip(2, false);
}
| POP_DW constlist_16bit trailing_comma
;
dl:
POP_DL {
sect_Skip(4, false);
}
| POP_DL constlist_32bit trailing_comma
;
sm83_adc:
SM83_ADC op_a_n {
sect_ConstByte(0xCE);
@@ -1996,7 +1903,7 @@ sm83_and:
sm83_bit:
SM83_BIT reloc_3bit COMMA reg_r {
uint8_t mask = static_cast<uint8_t>(0x40 | $4);
$2.makeCheckBitIndex(mask);
$2.addCheckBitIndex(mask);
sect_ConstByte(0xCB);
if (!$2.isKnown()) {
sect_RelByte($2, 0);
@@ -2129,26 +2036,22 @@ sm83_ldd:
sm83_ldh:
SM83_LDH MODE_A COMMA op_mem_ind {
if ($4.makeCheckHRAM()) {
warning(
WARNING_OBSOLETE,
"LDH is deprecated with values from $00 to $FF; use $FF00 to $FFFF"
);
}
$4.addCheckHRAM();
sect_ConstByte(0xF0);
if (!$4.isKnown()) {
sect_RelByte($4, 1);
} else {
sect_ConstByte($4.value());
}
}
| SM83_LDH op_mem_ind COMMA MODE_A {
if ($2.makeCheckHRAM()) {
warning(
WARNING_OBSOLETE,
"LDH is deprecated with values from $00 to $FF; use $FF00 to $FFFF"
);
}
$2.addCheckHRAM();
sect_ConstByte(0xE0);
if (!$2.isKnown()) {
sect_RelByte($2, 1);
} else {
sect_ConstByte($2.value());
}
}
| SM83_LDH MODE_A COMMA c_ind {
sect_ConstByte(0xF2);
@@ -2170,7 +2073,7 @@ ff00_c_ind:
LBRACK relocexpr OP_ADD MODE_C RBRACK {
// This has to use `relocexpr`, not `iconst`, to avoid a shift/reduce conflict
if ($2.getConstVal() != 0xFF00) {
::error("Base value must be equal to $FF00 for $FF00+C");
::error("Base value must be equal to $FF00 for [$FF00+C]");
}
}
;
@@ -2197,7 +2100,7 @@ sm83_ld_hl:
}
| SM83_LD MODE_HL COMMA reg_tt_no_af {
::error(
"LD HL, %s is not a valid instruction; use LD H, %s and LD L, %s",
"\"LD HL, %s\" is not a valid instruction; use \"LD H, %s\" and \"LD L, %s\"",
reg_tt_names[$4],
reg_tt_high_names[$4],
reg_tt_low_names[$4]
@@ -2210,7 +2113,7 @@ sm83_ld_sp:
sect_ConstByte(0xF9);
}
| SM83_LD MODE_SP COMMA reg_bc_or_de {
::error("LD SP, %s is not a valid instruction", reg_tt_names[$4]);
::error("\"LD SP, %s\" is not a valid instruction", reg_tt_names[$4]);
}
| SM83_LD MODE_SP COMMA reloc_16bit {
sect_ConstByte(0x01 | (REG_SP << 4));
@@ -2233,10 +2136,6 @@ sm83_ld_c_ind:
SM83_LD ff00_c_ind COMMA MODE_A {
sect_ConstByte(0xE2);
}
| SM83_LD c_ind COMMA MODE_A {
warning(WARNING_OBSOLETE, "LD [C], A is deprecated; use LDH [C], A");
sect_ConstByte(0xE2);
}
;
sm83_ld_rr:
@@ -2252,7 +2151,7 @@ sm83_ld_r_no_a:
}
| SM83_LD reg_r_no_a COMMA reg_r {
if ($2 == REG_HL_IND && $4 == REG_HL_IND) {
::error("LD [HL], [HL] is not a valid instruction");
::error("\"LD [HL], [HL]\" is not a valid instruction");
} else {
sect_ConstByte(0x40 | ($2 << 3) | $4);
}
@@ -2270,10 +2169,6 @@ sm83_ld_a:
| SM83_LD reg_a COMMA ff00_c_ind {
sect_ConstByte(0xF2);
}
| SM83_LD reg_a COMMA c_ind {
warning(WARNING_OBSOLETE, "LD A, [C] is deprecated; use LDH A, [C]");
sect_ConstByte(0xF2);
}
| SM83_LD reg_a COMMA reg_rr {
sect_ConstByte(0x0A | ($4 << 4));
}
@@ -2290,7 +2185,7 @@ sm83_ld_ss:
}
| SM83_LD reg_bc_or_de COMMA reg_tt_no_af {
::error(
"LD %s, %s is not a valid instruction; use LD %s, %s and LD %s, %s",
"\"LD %s, %s\" is not a valid instruction; use \"LD %s, %s\" and \"LD %s, %s\"",
reg_tt_names[$2],
reg_tt_names[$4],
reg_tt_high_names[$2],
@@ -2334,7 +2229,7 @@ sm83_push:
sm83_res:
SM83_RES reloc_3bit COMMA reg_r {
uint8_t mask = static_cast<uint8_t>(0x80 | $4);
$2.makeCheckBitIndex(mask);
$2.addCheckBitIndex(mask);
sect_ConstByte(0xCB);
if (!$2.isKnown()) {
sect_RelByte($2, 0);
@@ -2413,7 +2308,7 @@ sm83_rrca:
sm83_rst:
SM83_RST reloc_8bit {
$2.makeCheckRST();
$2.addCheckRST();
if (!$2.isKnown()) {
sect_RelByte($2, 0);
} else {
@@ -2441,7 +2336,7 @@ sm83_scf:
sm83_set:
SM83_SET reloc_3bit COMMA reg_r {
uint8_t mask = static_cast<uint8_t>(0xC0 | $4);
$2.makeCheckBitIndex(mask);
$2.addCheckBitIndex(mask);
sect_ConstByte(0xCB);
if (!$2.isKnown()) {
sect_RelByte($2, 0);
@@ -2544,7 +2439,7 @@ op_sp_offset:
$$.checkNBit(8);
}
| %empty {
::error("LD HL, SP is not a valid instruction; use LD HL, SP + 0");
::error("\"LD HL, SP\" is not a valid instruction; use \"LD HL, SP + 0\"");
}
;

View File

@@ -5,12 +5,17 @@
#include <inttypes.h>
#include <limits.h>
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include "helpers.hpp" // assume, clz, ctz
#include "helpers.hpp" // assume
#include "linkdefs.hpp"
#include "opmath.hpp"
#include "asm/output.hpp"
@@ -20,24 +25,6 @@
using namespace std::literals;
void Expression::clear() {
data = 0;
isSymbol = false;
rpn.clear();
rpnPatchSize = 0;
}
uint8_t *Expression::reserveSpace(uint32_t size) {
return reserveSpace(size, size);
}
uint8_t *Expression::reserveSpace(uint32_t size, uint32_t patchSize) {
rpnPatchSize += patchSize;
size_t curSize = rpn.size();
rpn.resize(curSize + size);
return &rpn[curSize];
}
int32_t Expression::getConstVal() const {
if (!isKnown()) {
error("Expected constant expression: %s", std::get<std::string>(data).c_str());
@@ -47,10 +34,10 @@ int32_t Expression::getConstVal() const {
}
Symbol const *Expression::symbolOf() const {
if (!isSymbol) {
if (rpn.size() != 1 || rpn[0].command != RPN_SYM) {
return nullptr;
}
return sym_FindScopedSymbol(reinterpret_cast<char const *>(&rpn[1]));
return sym_FindScopedSymbol(std::get<std::string>(rpn[0].data));
}
bool Expression::isDiffConstant(Symbol const *sym) const {
@@ -67,40 +54,33 @@ bool Expression::isDiffConstant(Symbol const *sym) const {
}
void Expression::makeNumber(uint32_t value) {
clear();
assume(rpn.empty());
data = static_cast<int32_t>(value);
}
void Expression::makeSymbol(std::string const &symName) {
clear();
assume(rpn.empty());
if (Symbol *sym = sym_FindScopedSymbol(symName); sym_IsPC(sym) && !sect_GetSymbolSection()) {
error("PC has no value outside of a section");
data = 0;
} else if (sym && !sym->isNumeric() && !sym->isLabel()) {
error("'%s' is not a numeric symbol", symName.c_str());
error("`%s` is not a numeric symbol", symName.c_str());
data = 0;
} else if (!sym || !sym->isConstant()) {
isSymbol = true;
data = sym_IsPC(sym) ? "PC is not constant at assembly time"
: sym_IsPurgedScoped(symName)
? "'"s + symName + "' is not constant at assembly time; it was purged"
: "'"s + symName + "' is not constant at assembly time";
: (sym && sym->isDefined()
? "`"s + symName + "` is not constant at assembly time"
: "undefined symbol `"s + symName + "`")
+ (sym_IsPurgedScoped(symName) ? "; it was purged" : "");
sym = sym_Ref(symName);
size_t nameLen = sym->name.length() + 1; // Don't forget NUL!
// 1-byte opcode + 4-byte symbol ID
uint8_t *ptr = reserveSpace(nameLen + 1, 5);
*ptr++ = RPN_SYM;
memcpy(ptr, sym->name.c_str(), nameLen);
rpn.emplace_back(RPN_SYM, sym->name);
} else {
data = static_cast<int32_t>(sym->getConstantValue());
}
}
void Expression::makeBankSymbol(std::string const &symName) {
clear();
assume(rpn.empty());
if (Symbol const *sym = sym_FindScopedSymbol(symName); sym_IsPC(sym)) {
// The @ symbol is treated differently.
if (std::optional<uint32_t> outputBank = sect_GetOutputBank(); !outputBank) {
@@ -108,98 +88,68 @@ void Expression::makeBankSymbol(std::string const &symName) {
data = 1;
} else if (*outputBank == UINT32_MAX) {
data = "Current section's bank is not known";
*reserveSpace(1) = RPN_BANK_SELF;
rpn.emplace_back(RPN_BANK_SELF);
} else {
data = static_cast<int32_t>(*outputBank);
}
return;
} else if (sym && !sym->isLabel()) {
error("BANK argument must be a label");
error("`BANK` argument must be a label");
data = 1;
} else {
sym = sym_Ref(symName);
assume(sym); // If the symbol didn't exist, it should have been created
if (sym->getSection() && sym->getSection()->bank != UINT32_MAX) {
// Symbol's section is known and bank is fixed
data = static_cast<int32_t>(sym->getSection()->bank);
} else {
data = sym_IsPurgedScoped(symName)
? "\""s + symName + "\"'s bank is not known; it was purged"
: "\""s + symName + "\"'s bank is not known";
size_t nameLen = sym->name.length() + 1; // Room for NUL!
// 1-byte opcode + 4-byte sect ID
uint8_t *ptr = reserveSpace(nameLen + 1, 5);
*ptr++ = RPN_BANK_SYM;
memcpy(ptr, sym->name.c_str(), nameLen);
? "`"s + symName + "`'s bank is not known; it was purged"
: "`"s + symName + "`'s bank is not known";
rpn.emplace_back(RPN_BANK_SYM, sym->name);
}
}
}
void Expression::makeBankSection(std::string const &sectName) {
clear();
assume(rpn.empty());
if (Section *sect = sect_FindSectionByName(sectName); sect && sect->bank != UINT32_MAX) {
data = static_cast<int32_t>(sect->bank);
} else {
data = "Section \""s + sectName + "\"'s bank is not known";
size_t nameLen = sectName.length() + 1; // Room for NUL!
uint8_t *ptr = reserveSpace(nameLen + 1);
*ptr++ = RPN_BANK_SECT;
memcpy(ptr, sectName.data(), nameLen);
rpn.emplace_back(RPN_BANK_SECT, sectName);
}
}
void Expression::makeSizeOfSection(std::string const &sectName) {
clear();
assume(rpn.empty());
if (Section *sect = sect_FindSectionByName(sectName); sect && sect->isSizeKnown()) {
data = static_cast<int32_t>(sect->size);
} else {
data = "Section \""s + sectName + "\"'s size is not known";
size_t nameLen = sectName.length() + 1; // Room for NUL!
uint8_t *ptr = reserveSpace(nameLen + 1);
*ptr++ = RPN_SIZEOF_SECT;
memcpy(ptr, sectName.data(), nameLen);
rpn.emplace_back(RPN_SIZEOF_SECT, sectName);
}
}
void Expression::makeStartOfSection(std::string const &sectName) {
clear();
assume(rpn.empty());
if (Section *sect = sect_FindSectionByName(sectName); sect && sect->org != UINT32_MAX) {
data = static_cast<int32_t>(sect->org);
} else {
data = "Section \""s + sectName + "\"'s start is not known";
size_t nameLen = sectName.length() + 1; // Room for NUL!
uint8_t *ptr = reserveSpace(nameLen + 1);
*ptr++ = RPN_STARTOF_SECT;
memcpy(ptr, sectName.data(), nameLen);
rpn.emplace_back(RPN_STARTOF_SECT, sectName);
}
}
void Expression::makeSizeOfSectionType(SectionType type) {
clear();
assume(rpn.empty());
data = "Section type's size is not known";
uint8_t *ptr = reserveSpace(2);
*ptr++ = RPN_SIZEOF_SECTTYPE;
*ptr = type;
rpn.emplace_back(RPN_SIZEOF_SECTTYPE, static_cast<uint8_t>(type));
}
void Expression::makeStartOfSectionType(SectionType type) {
clear();
assume(rpn.empty());
data = "Section type's start is not known";
uint8_t *ptr = reserveSpace(2);
*ptr++ = RPN_STARTOF_SECTTYPE;
*ptr = type;
rpn.emplace_back(RPN_STARTOF_SECTTYPE, static_cast<uint8_t>(type));
}
static bool tryConstZero(Expression const &lhs, Expression const &rhs) {
@@ -253,7 +203,7 @@ static int32_t tryConstLow(Expression const &expr) {
assume(sect.org == UINT32_MAX);
int32_t symbolOfs = sym->getValue() + 1;
return (symbolOfs + sect.alignOfs) & 0xFF;
return op_low(symbolOfs + sect.alignOfs);
}
// Returns a constant binary AND with one non-constant operand, or -1 if it cannot be computed.
@@ -297,16 +247,13 @@ static int32_t tryConstMask(Expression const &lhs, Expression const &rhs) {
}
void Expression::makeUnaryOp(RPNCommand op, Expression &&src) {
clear();
assume(rpn.empty());
// First, check if the expression is known
if (src.isKnown()) {
// If the expressions is known, just compute the value
int32_t val = src.value();
uint32_t uval = static_cast<uint32_t>(val);
switch (op) {
switch (int32_t val = src.value(); op) {
case RPN_NEG:
data = static_cast<int32_t>(-uval);
data = op_neg(val);
break;
case RPN_NOT:
data = ~val;
@@ -315,16 +262,16 @@ void Expression::makeUnaryOp(RPNCommand op, Expression &&src) {
data = !val;
break;
case RPN_HIGH:
data = static_cast<int32_t>(uval >> 8 & 0xFF);
data = op_high(val);
break;
case RPN_LOW:
data = val & 0xFF;
data = op_low(val);
break;
case RPN_BITWIDTH:
data = val != 0 ? 32 - clz(uval) : 0;
data = op_bitwidth(val);
break;
case RPN_TZCOUNT:
data = val != 0 ? ctz(uval) : 32;
data = op_tzcount(val);
break;
// LCOV_EXCL_START
default:
@@ -337,16 +284,15 @@ void Expression::makeUnaryOp(RPNCommand op, Expression &&src) {
} else if (int32_t constVal; op == RPN_LOW && (constVal = tryConstLow(src)) != -1) {
data = constVal;
} else {
// If it's not known, just reuse its RPN buffer and append the operator
rpnPatchSize = src.rpnPatchSize;
std::swap(rpn, src.rpn);
// If it's not known, just reuse its RPN vector and append the operator
data = std::move(src.data);
*reserveSpace(1) = op;
std::swap(rpn, src.rpn);
rpn.emplace_back(op);
}
}
void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const &src2) {
clear();
assume(rpn.empty());
// First, check if the expressions are known
if (src1.isKnown() && src2.isKnown()) {
// If both expressions are known, just compute the value
@@ -397,37 +343,30 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
if (rval < 0) {
warning(WARNING_SHIFT_AMOUNT, "Shifting left by negative amount %" PRId32, rval);
}
if (rval >= 32) {
warning(WARNING_SHIFT_AMOUNT, "Shifting left by large amount %" PRId32, rval);
}
data = op_shift_left(lval, rval);
break;
case RPN_SHR:
if (lval < 0) {
warning(WARNING_SHIFT, "Shifting right negative value %" PRId32, lval);
}
if (rval < 0) {
warning(WARNING_SHIFT_AMOUNT, "Shifting right by negative amount %" PRId32, rval);
}
if (rval >= 32) {
warning(WARNING_SHIFT_AMOUNT, "Shifting right by large amount %" PRId32, rval);
}
data = op_shift_right(lval, rval);
break;
case RPN_USHR:
if (rval < 0) {
warning(WARNING_SHIFT_AMOUNT, "Shifting right by negative amount %" PRId32, rval);
}
if (rval >= 32) {
warning(WARNING_SHIFT_AMOUNT, "Shifting right by large amount %" PRId32, rval);
}
data = op_shift_right_unsigned(lval, rval);
break;
case RPN_MUL:
@@ -437,7 +376,6 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
if (rval == 0) {
fatal("Division by zero");
}
if (lval == INT32_MIN && rval == -1) {
warning(
WARNING_DIV,
@@ -454,7 +392,6 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
if (rval == 0) {
fatal("Modulo by zero");
}
if (lval == INT32_MIN && rval == -1) {
data = 0;
} else {
@@ -465,7 +402,6 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
if (rval < 0) {
fatal("Exponentiation by negative power");
}
data = op_exponent(lval, rval);
break;
// LCOV_EXCL_START
@@ -488,88 +424,56 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
// Convert the left-hand expression if it's constant
if (src1.isKnown()) {
uint32_t lval = src1.value();
uint8_t bytes[] = {
RPN_CONST,
static_cast<uint8_t>(lval),
static_cast<uint8_t>(lval >> 8),
static_cast<uint8_t>(lval >> 16),
static_cast<uint8_t>(lval >> 24),
};
rpn.clear();
rpnPatchSize = 0;
memcpy(reserveSpace(sizeof(bytes)), bytes, sizeof(bytes));
// Use the other expression's un-const reason
data = std::move(src2.data);
rpn.emplace_back(RPN_CONST, lval);
} else {
// Otherwise just reuse its RPN buffer
rpnPatchSize = src1.rpnPatchSize;
std::swap(rpn, src1.rpn);
// Otherwise just reuse its RPN vector
data = std::move(src1.data);
std::swap(rpn, src1.rpn);
}
// Now, merge the right expression into the left one
if (src2.isKnown()) {
// If the right expression is constant, append a shim instead
// If the right expression is constant, append its value
uint32_t rval = src2.value();
uint8_t bytes[] = {
RPN_CONST,
static_cast<uint8_t>(rval),
static_cast<uint8_t>(rval >> 8),
static_cast<uint8_t>(rval >> 16),
static_cast<uint8_t>(rval >> 24),
};
uint8_t *ptr = reserveSpace(sizeof(bytes) + 1, sizeof(bytes) + 1);
memcpy(ptr, bytes, sizeof(bytes));
ptr[sizeof(bytes)] = op;
rpn.emplace_back(RPN_CONST, rval);
} else {
// Copy the right RPN and append the operator
uint32_t rightRpnSize = src2.rpn.size();
uint8_t *ptr = reserveSpace(rightRpnSize + 1, src2.rpnPatchSize + 1);
if (rightRpnSize > 0) {
// If `rightRpnSize == 0`, then `memcpy(ptr, nullptr, rightRpnSize)` would be UB
memcpy(ptr, src2.rpn.data(), rightRpnSize);
}
ptr[rightRpnSize] = op;
// Otherwise just extend with its RPN vector
rpn.insert(rpn.end(), RANGE(src2.rpn));
}
// Append the operator
rpn.emplace_back(op);
}
}
bool Expression::makeCheckHRAM() {
isSymbol = false;
void Expression::addCheckHRAM() {
if (!isKnown()) {
*reserveSpace(1) = RPN_HRAM;
rpn.emplace_back(RPN_HRAM);
} else if (int32_t val = value(); val >= 0xFF00 && val <= 0xFFFF) {
// That range is valid, but only keep the lower byte
// That range is valid; only keep the lower byte
data = val & 0xFF;
} else if (val >= 0 && val <= 0xFF) {
// That range is valid, but deprecated
return true;
} else {
error("Source address $%" PRIx32 " not between $FF00 to $FFFF", val);
}
return false;
}
void Expression::makeCheckRST() {
void Expression::addCheckRST() {
if (!isKnown()) {
*reserveSpace(1) = RPN_RST;
rpn.emplace_back(RPN_RST);
} else if (int32_t val = value(); val & ~0x38) {
// A valid RST address must be masked with 0x38
error("Invalid address $%" PRIx32 " for RST", val);
error("Invalid address $%" PRIx32 " for `RST`", val);
}
}
void Expression::makeCheckBitIndex(uint8_t mask) {
void Expression::addCheckBitIndex(uint8_t mask) {
assume((mask & 0xC0) != 0x00); // The high two bits must correspond to BIT, RES, or SET
if (!isKnown()) {
uint8_t *ptr = reserveSpace(2);
*ptr++ = RPN_BIT_INDEX;
*ptr = mask;
rpn.emplace_back(RPN_BIT_INDEX, mask);
} else if (int32_t val = value(); val & ~0x07) {
// A valid bit index must be masked with 0x07
static char const *instructions[4] = {"instruction", "BIT", "RES", "SET"};
static char const *instructions[4] = {"instruction", "`BIT`", "`RES`", "`SET`"};
error("Invalid bit index %" PRId32 " for %s", val, instructions[mask >> 6]);
}
}
@@ -591,7 +495,7 @@ bool checkNBit(int32_t v, uint8_t n, char const *name) {
"%s must be %u-bit%s",
name ? name : "Expression",
n,
n == 8 && !name ? "; use LOW() to force 8-bit" : ""
n == 8 && !name ? "; use `LOW()` to force 8-bit" : ""
);
return false;
}
@@ -601,10 +505,111 @@ bool checkNBit(int32_t v, uint8_t n, char const *name) {
"%s must be %u-bit%s",
name ? name : "Expression",
n,
n == 8 && !name ? "; use LOW() to force 8-bit" : ""
n == 8 && !name ? "; use `LOW()` to force 8-bit" : ""
);
return false;
}
return true;
}
void Expression::encode(std::vector<uint8_t> &buffer) const {
assume(buffer.empty());
if (isKnown()) {
// If the RPN expression's value is known, output a constant directly
uint32_t val = value();
buffer.resize(5);
buffer[0] = RPN_CONST;
buffer[1] = val & 0xFF;
buffer[2] = val >> 8;
buffer[3] = val >> 16;
buffer[4] = val >> 24;
} else {
// If the RPN expression's value is not known, serialize its RPN values
buffer.reserve(rpn.size() * 2); // Rough estimate of the serialized size
for (RPNValue const &val : rpn) {
val.appendEncoded(buffer);
}
}
}
RPNValue::RPNValue(RPNCommand cmd) : command(cmd), data(std::monostate{}) {
assume(
cmd != RPN_SIZEOF_SECTTYPE && cmd != RPN_STARTOF_SECTTYPE && cmd != RPN_BIT_INDEX
&& cmd != RPN_CONST && cmd != RPN_SYM && cmd != RPN_BANK_SYM && cmd != RPN_BANK_SECT
&& cmd != RPN_SIZEOF_SECT && cmd != RPN_STARTOF_SECT
);
}
RPNValue::RPNValue(RPNCommand cmd, uint8_t val) : command(cmd), data(val) {
assume(cmd == RPN_SIZEOF_SECTTYPE || cmd == RPN_STARTOF_SECTTYPE || cmd == RPN_BIT_INDEX);
}
RPNValue::RPNValue(RPNCommand cmd, uint32_t val) : command(cmd), data(val) {
assume(cmd == RPN_CONST);
}
RPNValue::RPNValue(RPNCommand cmd, std::string const &name) : command(cmd), data(name) {
assume(
cmd == RPN_SYM || cmd == RPN_BANK_SYM || cmd == RPN_BANK_SECT || cmd == RPN_SIZEOF_SECT
|| cmd == RPN_STARTOF_SECT
);
}
void RPNValue::appendEncoded(std::vector<uint8_t> &buffer) const {
// Every command starts with its own ID
buffer.push_back(command);
switch (command) {
case RPN_CONST: {
// The command ID is followed by a four-byte integer
assume(std::holds_alternative<uint32_t>(data));
uint32_t val = std::get<uint32_t>(data);
buffer.push_back(val & 0xFF);
buffer.push_back(val >> 8);
buffer.push_back(val >> 16);
buffer.push_back(val >> 24);
break;
}
case RPN_SYM:
case RPN_BANK_SYM: {
// The command ID is followed by a four-byte symbol ID
assume(std::holds_alternative<std::string>(data));
// The symbol name is always written expanded
Symbol *sym = sym_FindExactSymbol(std::get<std::string>(data));
out_RegisterSymbol(*sym); // Ensure that `sym->ID` is set
buffer.push_back(sym->ID & 0xFF);
buffer.push_back(sym->ID >> 8);
buffer.push_back(sym->ID >> 16);
buffer.push_back(sym->ID >> 24);
break;
}
case RPN_BANK_SECT:
case RPN_SIZEOF_SECT:
case RPN_STARTOF_SECT: {
// The command ID is followed by a NUL-terminated section name string
assume(std::holds_alternative<std::string>(data));
std::string const &name = std::get<std::string>(data);
buffer.reserve(buffer.size() + name.length() + 1);
buffer.insert(buffer.end(), RANGE(name));
buffer.push_back('\0');
break;
}
case RPN_SIZEOF_SECTTYPE:
case RPN_STARTOF_SECTTYPE:
case RPN_BIT_INDEX:
// The command ID is followed by a byte value
assume(std::holds_alternative<uint8_t>(data));
buffer.push_back(std::get<uint8_t>(data));
break;
default:
// Other command IDs are not followed by anything
assume(std::holds_alternative<std::monostate>(data));
break;
}
}

View File

@@ -3,15 +3,23 @@
#include "asm/section.hpp"
#include <algorithm>
#include <deque>
#include <errno.h>
#include <inttypes.h>
#include <iterator>
#include <optional>
#include <stack>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <utility>
#include <vector>
#include "helpers.hpp"
#include "itertools.hpp" // InsertionOrderedMap
#include "linkdefs.hpp"
#include "asm/fstack.hpp"
#include "asm/lexer.hpp"
@@ -38,8 +46,7 @@ struct SectionStackEntry {
};
static Section *currentSection = nullptr;
static std::deque<Section> sectionList;
static std::unordered_map<std::string, size_t> sectionMap; // Indexes into `sectionList`
static InsertionOrderedMap<Section> sections;
static uint32_t curOffset; // Offset into the current section (see `sect_GetSymbolOffset`)
@@ -57,7 +64,7 @@ static bool requireSection() {
return true;
}
error("Cannot output data outside of a SECTION");
error("Cannot output data outside of a `SECTION`");
return false;
}
@@ -67,31 +74,33 @@ static bool requireCodeSection() {
return false;
}
if (sect_HasData(currentSection->type)) {
if (sectTypeHasData(currentSection->type)) {
return true;
}
error(
"Section '%s' cannot contain code or data (not ROM0 or ROMX)", currentSection->name.c_str()
"Section \"%s\" cannot contain code or data (not `ROM0` or `ROMX`)",
currentSection->name.c_str()
);
return false;
}
size_t sect_CountSections() {
return sectionList.size();
return sections.size();
}
void sect_ForEach(void (*callback)(Section &)) {
for (Section &sect : sectionList) {
for (Section &sect : sections) {
callback(sect);
}
}
void sect_CheckSizes() {
for (Section const &sect : sectionList) {
for (Section const &sect : sections) {
if (uint32_t maxSize = sectionTypeInfo[sect.type].size; sect.size > maxSize) {
error(
"Section '%s' grew too big (max size = 0x%" PRIX32 " bytes, reached 0x%" PRIX32 ")",
"Section \"%s\" grew too big (max size = 0x%" PRIX32 " bytes, reached 0x%" PRIX32
")",
sect.name.c_str(),
maxSize,
sect.size
@@ -101,11 +110,10 @@ void sect_CheckSizes() {
}
Section *sect_FindSectionByName(std::string const &name) {
auto search = sectionMap.find(name);
return search != sectionMap.end() ? &sectionList[search->second] : nullptr;
auto index = sections.findIndex(name);
return index ? &sections[*index] : nullptr;
}
#define mask(align) ((1U << (align)) - 1)
#define sectError(...) \
do { \
error(__VA_ARGS__); \
@@ -115,13 +123,20 @@ Section *sect_FindSectionByName(std::string const &name) {
static unsigned int mergeSectUnion(
Section &sect, SectionType type, uint32_t org, uint8_t alignment, uint16_t alignOffset
) {
assume(alignment < 16); // Should be ensured by the caller
unsigned int nbSectErrors = 0;
assume(alignment < 16); // Should be ensured by the caller
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
assume(sect.align <= 16); // Left-shifting by 32 or more would be UB
uint32_t sectAlignSize = 1u << sect.align;
uint32_t sectAlignMask = sectAlignSize - 1;
// Unionized sections only need "compatible" constraints, and they end up with the strictest
// combination of both.
if (sect_HasData(type)) {
sectError("Cannot declare ROM sections as UNION");
if (sectTypeHasData(type)) {
sectError("Cannot declare ROM sections as `UNION`");
}
if (org != UINT32_MAX) {
@@ -130,10 +145,10 @@ static unsigned int mergeSectUnion(
sectError(
"Section already declared as fixed at different address $%04" PRIx32, sect.org
);
} else if (sect.align != 0 && (mask(sect.align) & (org - sect.alignOfs))) {
} else if (sect.align != 0 && ((org - sect.alignOfs) & sectAlignMask)) {
sectError(
"Section already declared as aligned to %u bytes (offset %" PRIu16 ")",
1U << sect.align,
"Section already declared as aligned to %" PRIu32 " bytes (offset %" PRIu16 ")",
sectAlignSize,
sect.alignOfs
);
} else {
@@ -144,17 +159,18 @@ static unsigned int mergeSectUnion(
} else if (alignment != 0) {
// Make sure any fixed address given is compatible
if (sect.org != UINT32_MAX) {
if ((sect.org - alignOffset) & mask(alignment)) {
if ((sect.org - alignOffset) & alignMask) {
sectError(
"Section already declared as fixed at incompatible address $%04" PRIx32,
sect.org
);
}
// Check if alignment offsets are compatible
} else if ((alignOffset & mask(sect.align)) != (sect.alignOfs & mask(alignment))) {
} else if ((alignOffset & sectAlignMask) != (sect.alignOfs & alignMask)) {
sectError(
"Section already declared with incompatible %u-byte alignment (offset %" PRIu16 ")",
1U << sect.align,
"Section already declared with incompatible %" PRIu32
"-byte alignment (offset %" PRIu16 ")",
sectAlignSize,
sect.alignOfs
);
} else if (alignment > sect.align) {
@@ -169,9 +185,16 @@ static unsigned int mergeSectUnion(
static unsigned int
mergeFragments(Section &sect, uint32_t org, uint8_t alignment, uint16_t alignOffset) {
assume(alignment < 16); // Should be ensured by the caller
unsigned int nbSectErrors = 0;
assume(alignment < 16); // Should be ensured by the caller
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
assume(sect.align <= 16); // Left-shifting by 32 or more would be UB
uint32_t sectAlignSize = 1u << sect.align;
uint32_t sectAlignMask = sectAlignSize - 1;
// Fragments only need "compatible" constraints, and they end up with the strictest
// combination of both.
// The merging is however performed at the *end* of the original section!
@@ -183,10 +206,10 @@ static unsigned int
sectError(
"Section already declared as fixed at incompatible address $%04" PRIx32, sect.org
);
} else if (sect.align != 0 && (mask(sect.align) & (curOrg - sect.alignOfs))) {
} else if (sect.align != 0 && ((curOrg - sect.alignOfs) & sectAlignMask)) {
sectError(
"Section already declared as aligned to %u bytes (offset %" PRIu16 ")",
1U << sect.align,
"Section already declared as aligned to %" PRIu32 " bytes (offset %" PRIu16 ")",
sectAlignSize,
sect.alignOfs
);
} else {
@@ -195,25 +218,20 @@ static unsigned int
}
} else if (alignment != 0) {
int32_t curOfs = (alignOffset - sect.size) % (1U << alignment);
if (curOfs < 0) {
curOfs += 1U << alignment;
}
// Make sure any fixed address given is compatible
if (sect.org != UINT32_MAX) {
if ((sect.org - curOfs) & mask(alignment)) {
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,
sect.org
);
}
// Check if alignment offsets are compatible
} else if ((curOfs & mask(sect.align)) != (sect.alignOfs & mask(alignment))) {
} else if ((curOfs & sectAlignMask) != (sect.alignOfs & alignMask)) {
sectError(
"Section already declared with incompatible %u-byte alignment (offset %" PRIu16 ")",
1U << sect.align,
"Section already declared with incompatible %" PRIu32
"-byte alignment (offset %" PRIu16 ")",
sectAlignSize,
sect.alignOfs
);
} else if (alignment > sect.align) {
@@ -239,12 +257,12 @@ static void mergeSections(
if (type != sect.type) {
sectError(
"Section already exists but with type %s", sectionTypeInfo[sect.type].name.c_str()
"Section already exists but with type `%s`", sectionTypeInfo[sect.type].name.c_str()
);
}
if (sect.modifier != mod) {
sectError("Section already declared as SECTION %s", sectionModNames[sect.modifier]);
sectError("Section already declared as `SECTION %s`", sectionModNames[sect.modifier]);
} else {
switch (mod) {
case SECTION_UNION:
@@ -266,9 +284,12 @@ static void mergeSections(
break;
case SECTION_NORMAL:
sectError([&]() {
fputs("Section already defined previously at ", stderr);
sect.src->dump(sect.fileLine);
errorNoTrace([&]() {
fputs("Section already defined\n", stderr);
fstk_TraceCurrent();
fputs(" and also:\n", stderr);
sect.src->printBacktrace(sect.fileLine);
++nbSectErrors;
});
break;
}
@@ -296,8 +317,7 @@ static Section *createSection(
SectionModifier mod
) {
// Add the new section to the list
Section &sect = sectionList.emplace_back();
sectionMap.emplace(name, sectionMap.size());
Section &sect = sections.add(name);
sect.name = name;
sect.type = type;
@@ -313,7 +333,7 @@ static Section *createSection(
out_RegisterNode(sect.src);
// It is only needed to allocate memory for ROM sections.
if (sect_HasData(type)) {
if (sectTypeHasData(type)) {
sect.data.resize(sectionTypeInfo[type].size);
}
@@ -321,9 +341,8 @@ static Section *createSection(
}
static Section *createSectionFragmentLiteral(Section const &parent) {
// Add the new section to the list, but do not update the map
Section &sect = sectionList.emplace_back();
assume(sectionMap.find(parent.name) != sectionMap.end());
assume(sections.contains(parent.name));
Section &sect = sections.addAnonymous();
sect.name = parent.name;
sect.type = parent.type;
@@ -339,7 +358,7 @@ static Section *createSectionFragmentLiteral(Section const &parent) {
out_RegisterNode(sect.src);
// Section fragment literals must be ROM sections.
assume(sect_HasData(sect.type));
assume(sectTypeHasData(sect.type));
sect.data.resize(sectionTypeInfo[sect.type].size);
return &sect;
@@ -356,12 +375,16 @@ static Section *getSection(
uint8_t alignment = attrs.alignment;
uint16_t alignOffset = attrs.alignOfs;
assume(alignment <= 16); // Should be ensured by the caller
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
// First, validate parameters, and normalize them if applicable
if (bank != UINT32_MAX) {
if (type != SECTTYPE_ROMX && type != SECTTYPE_VRAM && type != SECTTYPE_SRAM
&& type != SECTTYPE_WRAMX) {
error("BANK only allowed for ROMX, WRAMX, SRAM, or VRAM sections");
error("`BANK` only allowed for `ROMX`, `WRAMX`, `SRAM`, or `VRAM` sections");
} else if (bank < sectionTypeInfo[type].firstBank
|| bank > sectionTypeInfo[type].lastBank) {
error(
@@ -372,47 +395,44 @@ static Section *getSection(
sectionTypeInfo[type].lastBank
);
}
} else if (nbbanks(type) == 1) {
} else if (sectTypeBanks(type) == 1) {
// If the section type only has a single bank, implicitly force it
bank = sectionTypeInfo[type].firstBank;
}
if (alignOffset >= 1 << alignment) {
// This should be redundant, as the parser guarantees that `AlignmentSpec` will be valid.
if (alignOffset >= alignSize) {
// LCOV_EXCL_START
error(
"Alignment offset (%" PRIu16 ") must be smaller than alignment size (%u)",
"Alignment offset (%" PRIu16 ") must be smaller than alignment size (%" PRIu32 ")",
alignOffset,
1U << alignment
alignSize
);
alignOffset = 0;
// LCOV_EXCL_STOP
}
if (org != UINT32_MAX) {
if (org < sectionTypeInfo[type].startAddr || org > endaddr(type)) {
if (org < sectionTypeInfo[type].startAddr || org > sectTypeEndAddr(type)) {
error(
"Section \"%s\"'s fixed address $%04" PRIx32 " is outside of range [$%04" PRIx16
"; $%04" PRIx16 "]",
name.c_str(),
org,
sectionTypeInfo[type].startAddr,
endaddr(type)
sectTypeEndAddr(type)
);
}
}
if (alignment != 0) {
if (alignment > 16) {
error("Alignment must be between 0 and 16, not %u", alignment);
alignment = 16;
}
// It doesn't make sense to have both alignment and org set
uint32_t mask = mask(alignment);
if (org != UINT32_MAX) {
if ((org - alignOffset) & mask) {
error("Section \"%s\"'s fixed address doesn't match its alignment", name.c_str());
if ((org - alignOffset) & alignMask) {
error("Section \"%s\"'s fixed address does not match its alignment", name.c_str());
}
alignment = 0; // Ignore it if it's satisfied
} else if (sectionTypeInfo[type].startAddr & mask) {
} else if (sectionTypeInfo[type].startAddr & alignMask) {
error(
"Section \"%s\"'s alignment cannot be attained in %s",
name.c_str(),
@@ -443,7 +463,7 @@ static Section *getSection(
static void changeSection() {
if (!currentUnionStack.empty()) {
fatal("Cannot change the section within a UNION");
fatal("Cannot change the section within a `UNION`");
}
sym_ResetCurrentLabelScopes();
@@ -452,9 +472,9 @@ static void changeSection() {
uint32_t Section::getID() const {
// Section fragments share the same name but have different IDs, so search by identity
if (auto search =
std::find_if(RANGE(sectionList), [this](Section const &s) { return &s == this; });
search != sectionList.end()) {
return static_cast<uint32_t>(std::distance(sectionList.begin(), search));
std::find_if(RANGE(sections), [this](Section const &s) { return &s == this; });
search != sections.end()) {
return static_cast<uint32_t>(std::distance(sections.begin(), search));
}
return UINT32_MAX; // LCOV_EXCL_LINE
}
@@ -489,7 +509,7 @@ void sect_NewSection(
) {
for (SectionStackEntry &entry : sectionStack) {
if (entry.section && entry.section->name == name) {
fatal("Section '%s' is already on the stack", name.c_str());
fatal("Section \"%s\" is already on the stack", name.c_str());
}
}
@@ -521,7 +541,7 @@ void sect_SetLoadSection(
return;
}
if (sect_HasData(type)) {
if (sectTypeHasData(type)) {
error("`LOAD` blocks cannot create a ROM section");
return;
}
@@ -598,10 +618,10 @@ uint32_t sect_GetAlignBytes(uint8_t alignment, uint16_t offset) {
return 0;
}
// We need `(pcValue + curOffset + return value) % (1 << alignment) == offset`
// We need `(pcValue + curOffset + return value) & minAlignMask == offset`
uint16_t pcValue = isFixed ? sect->org : sect->alignOfs;
return static_cast<uint16_t>(offset - curOffset - pcValue)
% (1u << std::min(alignment, curAlignment));
uint32_t minAlignMask = (1u << std::min(alignment, curAlignment)) - 1;
return static_cast<uint16_t>(offset - curOffset - pcValue) & minAlignMask;
}
void sect_AlignPC(uint8_t alignment, uint16_t offset) {
@@ -609,11 +629,17 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
return;
}
assume(alignment <= 16); // Should be ensured by the caller
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
Section *sect = sect_GetSymbolSection();
uint32_t alignSize = 1 << alignment; // Size of an aligned "block"
assume(sect->align <= 16); // Left-shifting by 32 or more would be UB
uint32_t sectAlignSize = 1u << sect->align;
uint32_t sectAlignMask = sectAlignSize - 1;
if (sect->org != UINT32_MAX) {
if (uint32_t actualOffset = (sect->org + curOffset) % alignSize; actualOffset != offset) {
if (uint32_t actualOffset = (sect->org + curOffset) & alignMask; actualOffset != offset) {
error(
"Section is misaligned (at PC = $%04" PRIx32 ", expected ALIGN[%" PRIu32
", %" PRIu32 "], got ALIGN[%" PRIu32 ", %" PRIu32 "])",
@@ -624,33 +650,26 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
actualOffset
);
}
} else {
if (uint32_t actualOffset = (sect->alignOfs + curOffset) % alignSize,
sectAlignSize = 1 << sect->align;
sect->align != 0 && actualOffset % sectAlignSize != offset % sectAlignSize) {
} else if (uint32_t actualOffset = (sect->alignOfs + curOffset) & alignMask;
sect->align != 0 && (actualOffset & sectAlignMask) != (offset & sectAlignMask)) {
error(
"Section is misaligned ($%04" PRIx32
" bytes into the section, expected ALIGN[%" PRIu32 ", %" PRIu32
"], got ALIGN[%" PRIu32 ", %" PRIu32 "])",
"Section is misaligned ($%04" PRIx32 " bytes into the section, expected ALIGN[%" PRIu32
", %" PRIu32 "], got ALIGN[%" PRIu32 ", %" PRIu32 "])",
curOffset,
alignment,
offset,
alignment,
actualOffset
);
} else if (alignment >= 16) {
} else if (alignment == 16) {
// Treat an alignment large enough as fixing the address.
// Note that this also ensures that a section's alignment never becomes 16 or greater.
if (alignment > 16) {
error("Alignment must be between 0 and 16, not %u", alignment);
}
sect->align = 0; // Reset the alignment, since we're fixing the address.
sect->org = offset - curOffset;
} else if (alignment > sect->align) {
sect->align = alignment;
// We need `(sect->alignOfs + curOffset) % alignSize == offset`
sect->alignOfs = (offset - curOffset) % alignSize;
}
// We need `(sect->alignOfs + curOffset) & alignMask == offset`
sect->alignOfs = (offset - curOffset) & alignMask;
}
}
@@ -697,11 +716,11 @@ void sect_StartUnion() {
// your own peril! ^^
if (!currentSection) {
error("UNIONs must be inside a SECTION");
error("`UNION`s must be inside a `SECTION`");
return;
}
if (sect_HasData(currentSection->type)) {
error("Cannot use UNION inside of ROM0 or ROMX sections");
if (sectTypeHasData(currentSection->type)) {
error("Cannot use `UNION` inside of `ROM0` or `ROMX` sections");
return;
}
@@ -720,7 +739,7 @@ static void endUnionMember() {
void sect_NextUnionMember() {
if (currentUnionStack.empty()) {
error("Found NEXTU outside of a UNION construct");
error("Found `NEXTU` outside of a `UNION` construct");
return;
}
endUnionMember();
@@ -728,7 +747,7 @@ void sect_NextUnionMember() {
void sect_EndUnion() {
if (currentUnionStack.empty()) {
error("Found ENDU outside of a UNION construct");
error("Found `ENDU` outside of a `UNION` construct");
return;
}
endUnionMember();
@@ -738,7 +757,7 @@ void sect_EndUnion() {
void sect_CheckUnionClosed() {
if (!currentUnionStack.empty()) {
error("Unterminated UNION construct");
error("Unterminated `UNION` construct");
}
}
@@ -797,13 +816,13 @@ void sect_Skip(uint32_t skip, bool ds) {
return;
}
if (!sect_HasData(currentSection->type)) {
if (!sectTypeHasData(currentSection->type)) {
growSection(skip);
} else {
if (!ds) {
warning(
WARNING_EMPTY_DATA_DIRECTIVE,
"%s directive without data in ROM",
"`%s` directive without data in ROM",
(skip == 4) ? "DL"
: (skip == 2) ? "DW"
: "DB"
@@ -892,8 +911,8 @@ void sect_PCRelByte(Expression const &expr, uint32_t pcShift) {
if (offset < -128 || offset > 127) {
error(
"JR target must be between -128 and 127 bytes away, not %" PRId16
"; use JP instead",
"`JR` target must be between -128 and 127 bytes away, not %" PRId16
"; use `JP` instead",
offset
);
writeByte(0);
@@ -919,22 +938,28 @@ bool sect_BinaryFile(std::string const &name, uint32_t startPos) {
if (fseek(file, 0, SEEK_END) == 0) {
if (startPos > ftell(file)) {
error("Specified start position is greater than length of file '%s'", name.c_str());
error("Specified start position is greater than length of file \"%s\"", name.c_str());
return false;
}
// The file is seekable; skip to the specified start position
fseek(file, startPos, SEEK_SET);
} else {
// LCOV_EXCL_START
if (errno != ESPIPE) {
error("Error determining size of INCBIN file '%s': %s", name.c_str(), strerror(errno));
error(
"Error determining size of `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno)
);
}
// The file isn't seekable, so we'll just skip bytes one at a time
while (startPos--) {
if (fgetc(file) == EOF) {
error("Specified start position is greater than length of file '%s'", name.c_str());
error(
"Specified start position is greater than length of file \"%s\"", name.c_str()
);
return false;
}
}
// LCOV_EXCL_STOP
}
for (int byte; (byte = fgetc(file)) != EOF;) {
@@ -942,7 +967,9 @@ bool sect_BinaryFile(std::string const &name, uint32_t startPos) {
}
if (ferror(file)) {
error("Error reading INCBIN file '%s': %s", name.c_str(), strerror(errno));
// LCOV_EXCL_START
error("Error reading `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno));
// LCOV_EXCL_STOP
}
return false;
}
@@ -966,11 +993,11 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l
if (fseek(file, 0, SEEK_END) == 0) {
if (long fsize = ftell(file); startPos > fsize) {
error("Specified start position is greater than length of file '%s'", name.c_str());
error("Specified start position is greater than length of file \"%s\"", name.c_str());
return false;
} else if (startPos + length > fsize) {
error(
"Specified range in INCBIN file '%s' is out of bounds (%" PRIu32 " + %" PRIu32
"Specified range in `INCBIN` file \"%s\" is out of bounds (%" PRIu32 " + %" PRIu32
" > %ld)",
name.c_str(),
startPos,
@@ -982,29 +1009,37 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l
// The file is seekable; skip to the specified start position
fseek(file, startPos, SEEK_SET);
} else {
// LCOV_EXCL_START
if (errno != ESPIPE) {
error("Error determining size of INCBIN file '%s': %s", name.c_str(), strerror(errno));
error(
"Error determining size of `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno)
);
}
// The file isn't seekable, so we'll just skip bytes one at a time
while (startPos--) {
if (fgetc(file) == EOF) {
error("Specified start position is greater than length of file '%s'", name.c_str());
error(
"Specified start position is greater than length of file \"%s\"", name.c_str()
);
return false;
}
}
// LCOV_EXCL_STOP
}
while (length--) {
if (int byte = fgetc(file); byte != EOF) {
writeByte(byte);
// LCOV_EXCL_START
} else if (ferror(file)) {
error("Error reading INCBIN file '%s': %s", name.c_str(), strerror(errno));
error("Error reading `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno));
} else {
error(
"Premature end of INCBIN file '%s' (%" PRId32 " bytes left to read)",
"Premature end of `INCBIN` file \"%s\" (%" PRId32 " bytes left to read)",
name.c_str(),
length + 1
);
// LCOV_EXCL_STOP
}
}
return false;
@@ -1056,11 +1091,11 @@ void sect_CheckStack() {
void sect_EndSection() {
if (!currentSection) {
fatal("Cannot end the section outside of a SECTION");
fatal("Cannot end the section outside of a `SECTION`");
}
if (!currentUnionStack.empty()) {
fatal("Cannot end the section within a UNION");
fatal("Cannot end the section within a `UNION`");
}
if (currentLoadSection) {
@@ -1077,11 +1112,11 @@ std::string sect_PushSectionFragmentLiteral() {
// Like `requireCodeSection` but fatal
if (!currentSection) {
fatal("Cannot output fragment literals outside of a SECTION");
fatal("Cannot output fragment literals outside of a `SECTION`");
}
if (!sect_HasData(currentSection->type)) {
if (!sectTypeHasData(currentSection->type)) {
fatal(
"Section '%s' cannot contain fragment literals (not ROM0 or ROMX)",
"Section \"%s\" cannot contain fragment literals (not `ROM0` or `ROMX`)",
currentSection->name.c_str()
);
}

View File

@@ -5,9 +5,16 @@
#include <algorithm>
#include <errno.h>
#include <inttypes.h>
#include <memory>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <time.h>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <variant>
#include "diagnostics.hpp"
#include "helpers.hpp" // assume
@@ -17,7 +24,9 @@
#include "asm/fstack.hpp"
#include "asm/lexer.hpp"
#include "asm/macro.hpp"
#include "asm/main.hpp"
#include "asm/output.hpp"
#include "asm/section.hpp"
#include "asm/warning.hpp"
using namespace std::literals;
@@ -30,6 +39,7 @@ static Symbol const *localScope = nullptr; // Current section's local label sco
static Symbol *PCSymbol;
static Symbol *NARGSymbol;
static Symbol *SCOPESymbol;
static Symbol *globalScopeSymbol;
static Symbol *localScopeSymbol;
static Symbol *RSSymbol;
@@ -39,8 +49,6 @@ static char savedDATE[256];
static char savedTIMESTAMP_ISO8601_LOCAL[256];
static char savedTIMESTAMP_ISO8601_UTC[256];
static bool exportAll = false; // -E
bool sym_IsPC(Symbol const *sym) {
return sym == PCSymbol;
}
@@ -55,14 +63,27 @@ static int32_t NARGCallback() {
if (MacroArgs const *macroArgs = fstk_GetCurrentMacroArgs(); macroArgs) {
return macroArgs->nbArgs();
} else {
error("_NARG has no value outside of a macro");
error("`_NARG` has no value outside of a macro");
return 0;
}
}
static std::shared_ptr<std::string> SCOPECallback() {
if (localScope) {
return std::make_shared<std::string>("..");
} else if (globalScope) {
return std::make_shared<std::string>(".");
} else {
if (!sect_GetSymbolSection()) {
error("`__SCOPE__` has no value outside of a section");
}
return std::make_shared<std::string>("");
}
}
static std::shared_ptr<std::string> globalScopeCallback() {
if (!globalScope) {
error("\".\" has no value outside of a label scope");
error("`.` has no value outside of a label scope");
return std::make_shared<std::string>("");
}
return std::make_shared<std::string>(globalScope->name);
@@ -70,7 +91,7 @@ static std::shared_ptr<std::string> globalScopeCallback() {
static std::shared_ptr<std::string> localScopeCallback() {
if (!localScope) {
error("\"..\" has no value outside of a local label scope");
error("`..` has no value outside of a local label scope");
return std::make_shared<std::string>("");
}
return std::make_shared<std::string>(localScope->name);
@@ -114,14 +135,15 @@ std::shared_ptr<std::string> Symbol::getEqus() const {
return std::get<std::shared_ptr<std::string>>(data);
}
static void dumpFilename(Symbol const &sym) {
fputs(" at ", stderr);
// Meant to be called last in an `errorNoTrace` callback
static void printBacktraces(Symbol const &sym) {
putc('\n', stderr);
fstk_TraceCurrent();
fputs(" and also:\n", stderr);
if (sym.src) {
sym.src->dump(sym.fileLine);
} else if (sym.isBuiltin) {
fputs("<builtin>", stderr);
sym.src->printBacktrace(sym.fileLine);
} else {
fputs("<command-line>", stderr);
fprintf(stderr, " at <%s>\n", sym.isBuiltin ? "builtin" : "command-line");
}
}
@@ -142,37 +164,52 @@ static bool isValidIdentifier(std::string const &s) {
}
static void alreadyDefinedError(Symbol const &sym, char const *asType) {
if (sym.isBuiltin && !sym_FindScopedValidSymbol(sym.name)) {
// `DEF()` would return false, so we should not claim the symbol is already defined
error("'%s' is reserved for a built-in symbol", sym.name.c_str());
auto suggestion = [&]() {
std::string s;
if (auto const &contents = sym.type == SYM_EQUS ? sym.getEqus() : nullptr;
contents && isValidIdentifier(*contents)) {
s.append(" (should it be {interpolated} to define its contents \"");
s.append(*contents);
s.append("\"?)");
}
return s;
};
if (sym.isBuiltin) {
if (sym_FindScopedValidSymbol(sym.name)) {
if (std::string s = suggestion(); asType) {
error("`%s` already defined as built-in %s%s", sym.name.c_str(), asType, s.c_str());
} else {
error([&]() {
fprintf(stderr, "'%s' already defined", sym.name.c_str());
error("`%s` already defined as built-in%s", sym.name.c_str(), s.c_str());
}
} else {
// `DEF()` would return false, so we should not claim the symbol is already defined,
// nor suggest to interpolate it
if (asType) {
error("`%s` is reserved for a built-in %s symbol", sym.name.c_str(), asType);
} else {
error("`%s` is reserved for a built-in symbol", sym.name.c_str());
}
}
} else {
errorNoTrace([&]() {
fprintf(stderr, "`%s` already defined", sym.name.c_str());
if (asType) {
fprintf(stderr, " as %s", asType);
}
dumpFilename(sym);
if (sym.type != SYM_EQUS) {
return;
}
if (std::string const &contents = *sym.getEqus(); isValidIdentifier(contents)) {
fprintf(
stderr,
"\n (should it be {interpolated} to define its contents \"%s\"?)",
contents.c_str()
);
}
fputs(suggestion().c_str(), stderr);
printBacktraces(sym);
});
}
}
static void redefinedError(Symbol const &sym) {
assume(sym.isBuiltin);
if (!sym_FindScopedValidSymbol(sym.name)) {
// `DEF()` would return false, so we should not imply the symbol is already defined
error("'%s' is reserved for a built-in symbol", sym.name.c_str());
if (sym_FindScopedValidSymbol(sym.name)) {
error("Built-in symbol `%s` cannot be redefined", sym.name.c_str());
} else {
error("Built-in symbol '%s' cannot be redefined", sym.name.c_str());
// `DEF()` would return false, so we should not imply the symbol is already defined
error("`%s` is reserved for a built-in symbol", sym.name.c_str());
}
}
@@ -190,8 +227,9 @@ static Symbol &createSymbol(std::string const &symName) {
Symbol &sym = symbols[symName];
sym.name = symName;
sym.isExported = false;
sym.isBuiltin = false;
sym.isExported = false;
sym.isQuiet = false;
sym.section = nullptr;
sym.src = fstk_GetFileStack();
sym.fileLine = sym.src ? lexer_GetLineNo() : 0;
@@ -221,12 +259,12 @@ static bool isAutoScoped(std::string const &symName) {
// Check for nothing after the dot
if (dotPos == symName.length() - 1) {
fatal("'%s' is a nonsensical reference to an empty local label", symName.c_str());
fatal("`%s` is a nonsensical reference to an empty local label", symName.c_str());
}
// Check for more than one dot
if (symName.find('.', dotPos + 1) != std::string::npos) {
fatal("'%s' is a nonsensical reference to a nested local label", symName.c_str());
fatal("`%s` is a nonsensical reference to a nested local label", symName.c_str());
}
// Check for already-qualified local label
@@ -236,7 +274,7 @@ static bool isAutoScoped(std::string const &symName) {
// Check for unqualifiable local label
if (!globalScope) {
fatal("Unqualified local label '%s' in main scope", symName.c_str());
fatal("Unqualified local label `%s` in main scope", symName.c_str());
}
return true;
@@ -272,6 +310,10 @@ Symbol *sym_FindScopedValidSymbol(std::string const &symName) {
if (sym == localScopeSymbol && !localScope) {
return nullptr;
}
// `__SCOPE__` has no value outside of a section
if (sym == SCOPESymbol && !sect_GetSymbolSection()) {
return nullptr;
}
return sym;
}
@@ -285,19 +327,19 @@ void sym_Purge(std::string const &symName) {
if (!sym) {
if (sym_IsPurgedScoped(symName)) {
error("'%s' was already purged", symName.c_str());
error("Undefined symbol `%s` was already purged", symName.c_str());
} else {
error("'%s' not defined", symName.c_str());
error("Undefined symbol `%s`", symName.c_str());
}
} else if (sym->isBuiltin) {
error("Built-in symbol '%s' cannot be purged", symName.c_str());
error("Built-in symbol `%s` cannot be purged", symName.c_str());
} else if (sym->ID != UINT32_MAX) {
error("Symbol \"%s\" is referenced and thus cannot be purged", symName.c_str());
error("Symbol `%s` is referenced and thus cannot be purged", symName.c_str());
} else {
if (sym->isExported) {
warning(WARNING_PURGE_1, "Purging an exported symbol \"%s\"", symName.c_str());
warning(WARNING_PURGE_1, "Purging an exported symbol `%s`", symName.c_str());
} else if (sym->isLabel()) {
warning(WARNING_PURGE_2, "Purging a label \"%s\"", symName.c_str());
warning(WARNING_PURGE_2, "Purging a label `%s`", symName.c_str());
}
// Do not keep a reference to the label after purging it
if (sym == globalScope) {
@@ -336,13 +378,10 @@ uint32_t Symbol::getConstantValue() const {
}
if (sym_IsPC(this)) {
if (!getSection()) {
error("PC has no value outside of a section");
} else {
assume(getSection()); // There's no way to reach here from outside of a section
error("PC does not have a constant value; the current section is not fixed");
}
} else {
error("\"%s\" does not have a constant value", name.c_str());
error("`%s` does not have a constant value", name.c_str());
}
return 0;
}
@@ -377,9 +416,9 @@ static Symbol *createNonrelocSymbol(std::string const &symName, bool numeric) {
return nullptr; // Don't allow overriding the symbol, that'd be bad!
} else if (!numeric) {
// The symbol has already been referenced, but it's not allowed
error([&]() {
fprintf(stderr, "'%s' already referenced", symName.c_str());
dumpFilename(*sym);
errorNoTrace([&]() {
fprintf(stderr, "`%s` already referenced", symName.c_str());
printBacktraces(*sym);
});
return nullptr; // Don't allow overriding the symbol, that'd be bad!
}
@@ -408,7 +447,7 @@ Symbol *sym_RedefEqu(std::string const &symName, int32_t value) {
}
if (sym->isDefined() && sym->type != SYM_EQU) {
alreadyDefinedError(*sym, "non-EQU");
alreadyDefinedError(*sym, "non-`EQU`");
return nullptr;
} else if (sym->isBuiltin) {
redefinedError(*sym);
@@ -443,11 +482,11 @@ Symbol *sym_RedefString(std::string const &symName, std::shared_ptr<std::string>
if (sym->type != SYM_EQUS) {
if (sym->isDefined()) {
alreadyDefinedError(*sym, "non-EQUS");
alreadyDefinedError(*sym, "non-`EQUS`");
} else {
error([&]() {
fprintf(stderr, "'%s' already referenced", symName.c_str());
dumpFilename(*sym);
errorNoTrace([&]() {
fprintf(stderr, "`%s` already referenced", symName.c_str());
printBacktraces(*sym);
});
}
return nullptr;
@@ -497,13 +536,13 @@ static Symbol *addLabel(std::string const &symName) {
sym->type = SYM_LABEL;
sym->data = static_cast<int32_t>(sect_GetSymbolOffset());
// Don't export anonymous labels
if (exportAll && !symName.starts_with('!')) {
if (options.exportAll && !symName.starts_with('!')) {
sym->isExported = true;
}
sym->section = sect_GetSymbolSection();
if (sym && !sym->section) {
error("Label \"%s\" created outside of a SECTION", symName.c_str());
error("Label `%s` created outside of a `SECTION`", symName.c_str());
}
return sym;
@@ -573,7 +612,7 @@ std::string sym_MakeAnonLabelName(uint32_t ofs, bool neg) {
// LCOV_EXCL_START
error(
"Reference to anonymous label %" PRIu32 " after, when only %" PRIu32
" may still be created",
" can still be created",
ofs + 1,
UINT32_MAX - anonLabelID
);
@@ -590,7 +629,7 @@ void sym_Export(std::string const &symName) {
if (symName.starts_with('!')) {
// LCOV_EXCL_START
// The parser does not accept anonymous labels for an `EXPORT` directive
error("Anonymous labels cannot be exported");
error("Cannot export anonymous label");
return;
// LCOV_EXCL_STOP
}
@@ -599,12 +638,16 @@ void sym_Export(std::string const &symName) {
// If the symbol doesn't exist, create a ref that can be purged
if (!sym) {
warning(WARNING_EXPORT_UNDEFINED, "Exporting an undefined symbol `%s`", symName.c_str());
sym = sym_Ref(symName);
}
sym->isExported = true;
}
Symbol *sym_AddMacro(std::string const &symName, int32_t defLineNo, ContentSpan const &span) {
Symbol *sym_AddMacro(
std::string const &symName, int32_t defLineNo, ContentSpan const &span, bool isQuiet
) {
Symbol *sym = createNonrelocSymbol(symName, false);
if (!sym) {
@@ -613,6 +656,7 @@ Symbol *sym_AddMacro(std::string const &symName, int32_t defLineNo, ContentSpan
sym->type = SYM_MACRO;
sym->data = span;
sym->isQuiet = isQuiet;
sym->src = fstk_GetFileStack();
// The symbol is created at the line after the `ENDM`,
@@ -635,11 +679,6 @@ Symbol *sym_Ref(std::string const &symName) {
return sym;
}
// Set whether to export all relocatable symbols by default
void sym_SetExportAll(bool set) {
exportAll = set;
}
// Define the built-in symbols
void sym_Init(time_t now) {
PCSymbol = &createSymbol("@"s);
@@ -662,6 +701,11 @@ void sym_Init(time_t now) {
localScopeSymbol->data = localScopeCallback;
localScopeSymbol->isBuiltin = true;
SCOPESymbol = &createSymbol("__SCOPE__"s);
SCOPESymbol->type = SYM_EQUS;
SCOPESymbol->data = SCOPECallback;
SCOPESymbol->isBuiltin = true;
RSSymbol = sym_AddVar("_RS"s, 0);
RSSymbol->isBuiltin = true;
@@ -702,8 +746,22 @@ void sym_Init(time_t now) {
time_utc
);
sym_AddString("__TIME__"s, std::make_shared<std::string>(savedTIME))->isBuiltin = true;
sym_AddString("__DATE__"s, std::make_shared<std::string>(savedDATE))->isBuiltin = true;
Symbol *timeSymbol = &createSymbol("__TIME__"s);
timeSymbol->type = SYM_EQUS;
timeSymbol->data = []() {
warning(WARNING_OBSOLETE, "`__TIME__` is deprecated; use `__ISO_8601_LOCAL__`");
return std::make_shared<std::string>(savedTIME);
};
timeSymbol->isBuiltin = true;
Symbol *dateSymbol = &createSymbol("__DATE__"s);
dateSymbol->type = SYM_EQUS;
dateSymbol->data = []() {
warning(WARNING_OBSOLETE, "`__DATE__` is deprecated; use `__ISO_8601_LOCAL__`");
return std::make_shared<std::string>(savedDATE);
};
dateSymbol->isBuiltin = true;
sym_AddString(
"__ISO_8601_LOCAL__"s, std::make_shared<std::string>(savedTIMESTAMP_ISO8601_LOCAL)
)

View File

@@ -2,19 +2,16 @@
#include "asm/warning.hpp"
#include <functional>
#include <inttypes.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "diagnostics.hpp"
#include "helpers.hpp"
#include "itertools.hpp"
#include "style.hpp"
#include "asm/fstack.hpp"
#include "asm/lexer.hpp"
#include "asm/main.hpp"
// clang-format off: nested initializers
@@ -33,7 +30,8 @@ Diagnostics<WarningLevel, WarningID> warnings = {
{"empty-data-directive", LEVEL_ALL },
{"empty-macro-arg", LEVEL_EXTRA },
{"empty-strrpl", LEVEL_ALL },
{"large-constant", LEVEL_ALL },
{"export-undefined", LEVEL_ALL },
{"large-constant", LEVEL_DEFAULT },
{"macro-shift", LEVEL_EXTRA },
{"nested-comment", LEVEL_DEFAULT },
{"obsolete", LEVEL_DEFAULT },
@@ -54,8 +52,8 @@ Diagnostics<WarningLevel, WarningID> warnings = {
},
.paramWarnings = {
{WARNING_NUMERIC_STRING_1, WARNING_NUMERIC_STRING_2, 1},
{WARNING_PURGE_1, WARNING_PURGE_2, 1},
{WARNING_TRUNCATION_1, WARNING_TRUNCATION_2, 2},
{WARNING_PURGE_1, WARNING_PURGE_2, 2},
{WARNING_TRUNCATION_1, WARNING_TRUNCATION_2, 1},
{WARNING_UNMAPPED_CHAR_1, WARNING_UNMAPPED_CHAR_2, 1},
},
.state = DiagnosticsState<WarningID>(),
@@ -64,31 +62,41 @@ Diagnostics<WarningLevel, WarningID> warnings = {
// clang-format on
static void printDiag(
char const *fmt, va_list args, char const *type, char const *flagfmt, char const *flag
char const *fmt,
va_list args,
char const *type,
StyleColor color,
char const *flagfmt,
char const *flag
) {
fputs(type, stderr);
fputs(": ", stderr);
if (fstk_DumpCurrent()) {
fprintf(stderr, flagfmt, flag);
fputs("\n ", stderr);
}
style_Set(stderr, color, true);
fprintf(stderr, "%s: ", type);
style_Reset(stderr);
vfprintf(stderr, fmt, args);
if (flagfmt) {
style_Set(stderr, color, true);
putc(' ', stderr);
fprintf(stderr, flagfmt, flag);
}
putc('\n', stderr);
lexer_DumpStringExpansions();
fstk_TraceCurrent();
}
static void incrementErrors() {
// This intentionally makes 0 act as "unlimited"
warnings.incrementErrors();
if (warnings.nbErrors == options.maxErrors) {
style_Set(stderr, STYLE_RED, true);
fprintf(
stderr,
"Assembly aborted after the maximum of %" PRIu64 " error%s! (configure with "
"'-X/--max-errors')\n",
"Assembly aborted after the maximum of %" PRIu64 " error%s!",
warnings.nbErrors,
warnings.nbErrors == 1 ? "" : "s"
);
style_Set(stderr, STYLE_RED, false);
fputs(" (configure with '-X/--max-errors')\n", stderr);
style_Reset(stderr);
exit(1);
}
}
@@ -97,20 +105,17 @@ void error(char const *fmt, ...) {
va_list args;
va_start(args, fmt);
printDiag(fmt, args, "error", ":", nullptr);
printDiag(fmt, args, "error", STYLE_RED, nullptr, nullptr);
va_end(args);
incrementErrors();
}
void error(std::function<void()> callback) {
void errorNoTrace(std::function<void()> callback) {
style_Set(stderr, STYLE_RED, true);
fputs("error: ", stderr);
if (fstk_DumpCurrent()) {
fputs(":\n ", stderr);
}
style_Reset(stderr);
callback();
putc('\n', stderr);
lexer_DumpStringExpansions();
incrementErrors();
}
@@ -120,7 +125,7 @@ void fatal(char const *fmt, ...) {
va_list args;
va_start(args, fmt);
printDiag(fmt, args, "FATAL", ":", nullptr);
printDiag(fmt, args, "FATAL", STYLE_RED, nullptr, nullptr);
va_end(args);
exit(1);
@@ -128,12 +133,14 @@ void fatal(char const *fmt, ...) {
void requireZeroErrors() {
if (warnings.nbErrors != 0) {
style_Set(stderr, STYLE_RED, true);
fprintf(
stderr,
"Assembly aborted with %" PRIu64 " error%s!\n",
warnings.nbErrors,
warnings.nbErrors == 1 ? "" : "s"
);
style_Reset(stderr);
exit(1);
}
}
@@ -149,11 +156,11 @@ void warning(WarningID id, char const *fmt, ...) {
break;
case WarningBehavior::ENABLED:
printDiag(fmt, args, "warning", ": [-W%s]", flag);
printDiag(fmt, args, "warning", STYLE_YELLOW, "[-W%s]", flag);
break;
case WarningBehavior::ERROR:
printDiag(fmt, args, "error", ": [-Werror=%s]", flag);
printDiag(fmt, args, "error", STYLE_RED, "[-Werror=%s]", flag);
break;
}

33
src/backtrace.cpp Normal file
View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
#include "backtrace.hpp"
#include <optional>
#include <stdint.h>
#include "platform.hpp" // strcasecmp
#include "util.hpp" // parseWholeNumber
Tracing tracing;
bool trace_ParseTraceDepth(char const *arg) {
if (!strcasecmp(arg, "collapse")) {
tracing.collapse = true;
return true;
} else if (!strcasecmp(arg, "no-collapse")) {
tracing.collapse = false;
return true;
} else if (!strcasecmp(arg, "all")) {
tracing.loud = true;
return true;
} else if (!strcasecmp(arg, "no-all")) {
tracing.loud = false;
return true;
} else {
std::optional<uint64_t> depth = parseWholeNumber(arg);
if (depth) {
tracing.depth = *depth;
}
return depth.has_value();
}
}

156
src/cli.cpp Normal file
View File

@@ -0,0 +1,156 @@
#include "cli.hpp"
#include <errno.h>
#include <fstream>
#include <stdio.h>
#include <string.h>
#include <string>
#include <vector>
#include "extern/getopt.hpp"
#include "style.hpp"
#include "usage.hpp"
#include "util.hpp" // isBlankSpace
using namespace std::literals;
// Turn an at-file's contents into an argv that `getopt` can handle, appending them to `argPool`.
static std::vector<size_t>
readAtFile(std::string const &path, std::vector<char> &argPool, Usage usage) {
std::vector<size_t> argvOfs;
std::filebuf file;
if (!file.open(path, std::ios_base::in)) {
style_Set(stderr, STYLE_RED, true);
fputs("FATAL: ", stderr);
style_Reset(stderr);
fprintf(stderr, "Failed to open at-file \"%s\": %s\n", path.c_str(), strerror(errno));
usage.printAndExit(1);
}
for (;;) {
int c = file.sbumpc();
// First, discard any leading blank space
while (isBlankSpace(c)) {
c = file.sbumpc();
}
// If it's a comment, discard everything until EOL
if (c == '#') {
c = file.sbumpc();
while (c != EOF && !isNewline(c)) {
c = file.sbumpc();
}
}
if (c == EOF) {
return argvOfs;
} else if (isNewline(c)) {
continue; // Start processing the next line
}
// Alright, now we can parse the line
do {
argvOfs.push_back(argPool.size());
// Read one argument (until the next whitespace char).
// We know there is one because we already have its first character in `c`.
for (; c != EOF && !isWhitespace(c); c = file.sbumpc()) {
argPool.push_back(c);
}
argPool.push_back('\0');
// Discard blank space until the next argument (candidate)
while (isBlankSpace(c)) {
c = file.sbumpc();
}
} while (c != EOF && !isNewline(c)); // End if we reached EOL
}
}
void cli_ParseArgs(
int argc,
char *argv[],
char const *shortOpts,
option const *longOpts,
void (*parseArg)(int, char *),
Usage usage
) {
struct AtFileStackEntry {
int parentInd; // Saved offset into parent argv
std::vector<char *> argv; // This context's arg pointer vec
AtFileStackEntry(int parentInd_, std::vector<char *> argv_)
: parentInd(parentInd_), argv(argv_) {}
};
std::vector<AtFileStackEntry> atFileStack;
int curArgc = argc;
char **curArgv = argv;
std::string optString = "-"s + shortOpts; // Request position arguments with a leading '-'
std::vector<std::vector<char>> argPools;
for (;;) {
char *atFileName = nullptr;
for (int ch;
(ch = musl_getopt_long_only(curArgc, curArgv, optString.c_str(), longOpts)) != -1;) {
if (ch == 1 && musl_optarg[0] == '@') {
atFileName = &musl_optarg[1];
break;
} else {
parseArg(ch, musl_optarg);
}
}
if (atFileName) {
// We need to allocate a new arg pool for each at-file, so as not to invalidate pointers
// previous at-files may have generated to their own arg pools.
// But for the same reason, the arg pool must also outlive the at-file's stack entry!
std::vector<char> &argPool = argPools.emplace_back();
// Copy `argv[0]` for error reporting, and because option parsing skips it
AtFileStackEntry &stackEntry =
atFileStack.emplace_back(musl_optind, std::vector{atFileName});
// It would be nice to compute the char pointers on the fly, but reallocs don't allow
// that; so we must compute the offsets after the pool is fixed
std::vector<size_t> offsets = readAtFile(&musl_optarg[1], argPool, usage);
stackEntry.argv.reserve(offsets.size() + 2); // Avoid a bunch of reallocs
for (size_t ofs : offsets) {
stackEntry.argv.push_back(&argPool.data()[ofs]);
}
stackEntry.argv.push_back(nullptr); // Don't forget the arg vector terminator!
curArgc = stackEntry.argv.size() - 1;
curArgv = stackEntry.argv.data();
musl_optind = 1; // Don't use 0 because we're not scanning a different argv per se
} else {
if (musl_optind != curArgc) {
// This happens if `--` is passed, process the remaining arg(s) as positional
assume(musl_optind < curArgc);
for (int i = musl_optind; i < curArgc; ++i) {
parseArg(1, argv[i]); // Positional argument
}
}
// Pop off the top stack entry, or end parsing if none
if (atFileStack.empty()) {
break;
}
// OK to restore `optind` directly, because `optpos` must be 0 right now.
// (Providing 0 would be a "proper" reset, but we want to resume parsing)
musl_optind = atFileStack.back().parentInd;
atFileStack.pop_back();
if (atFileStack.empty()) {
curArgc = argc;
curArgv = argv;
} else {
std::vector<char *> &vec = atFileStack.back().argv;
curArgc = vec.size();
curArgv = vec.data();
}
}
}
}

View File

@@ -1,8 +1,23 @@
// SPDX-License-Identifier: MIT
#include "diagnostics.hpp"
#include <optional>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string>
#include <utility>
#include "helpers.hpp"
#include "style.hpp"
#include "util.hpp" // isDigit
void warnx(char const *fmt, ...) {
va_list ap;
style_Set(stderr, STYLE_YELLOW, true);
fputs("warning: ", stderr);
style_Reset(stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
@@ -48,25 +63,9 @@ std::pair<WarningState, std::optional<uint32_t>> getInitialWarningState(std::str
return {state, std::nullopt};
}
// Is the rest of the string a decimal number?
// We want to avoid `strtoul`'s whitespace and sign, so we parse manually
// If the rest of the string is a decimal number, it's the parameter value
char const *ptr = flag.c_str() + equals + 1;
uint32_t param = 0;
bool overflowed = false;
for (; *ptr >= '0' && *ptr <= '9'; ++ptr) {
if (overflowed) {
continue;
}
uint32_t c = *ptr - '0';
if (param > (UINT32_MAX - c) / 10) {
overflowed = true;
param = UINT32_MAX;
continue;
}
param = param * 10 + c;
}
uint64_t param = parseNumber(ptr, BASE_10).value_or(0);
// If we reached the end of the string, truncate it at the '='
if (*ptr == '\0') {
@@ -77,5 +76,5 @@ std::pair<WarningState, std::optional<uint32_t>> getInitialWarningState(std::str
}
}
return {state, param};
return {state, param > UINT32_MAX ? UINT32_MAX : param};
}

203
src/extern/getopt.cpp vendored
View File

@@ -11,27 +11,24 @@
#include <string.h>
#include <wchar.h>
#include "style.hpp"
char *musl_optarg;
int musl_optind = 1, musl_opterr = 1, musl_optopt;
int musl_optreset = 0;
int musl_optind = 1, musl_optopt;
static int musl_optpos;
static void musl_getopt_msg(char const *a, char const *b, char const *c, size_t l) {
FILE *f = stderr;
if (fputs(a, f) >= 0 && fwrite(b, strlen(b), 1, f) && fwrite(c, 1, l, f) == l) {
putc('\n', f);
}
static void musl_getopt_msg(char const *msg, char const *param) {
style_Set(stderr, STYLE_RED, true);
fputs("error: ", stderr);
style_Reset(stderr);
fputs(msg, stderr);
fputs(param, stderr);
putc('\n', stderr);
}
static int getopt(int argc, char *argv[], char const *optstring) {
int i;
wchar_t c, d;
int k, l;
char *optchar;
if (!musl_optind || musl_optreset) {
musl_optreset = 0;
static int musl_getopt(int argc, char *argv[], char const *optstring) {
if (!musl_optind) {
musl_optpos = 0;
musl_optind = 1;
}
@@ -40,7 +37,9 @@ static int getopt(int argc, char *argv[], char const *optstring) {
return -1;
}
if (argv[musl_optind][0] != '-') {
char *argi = argv[musl_optind];
if (argi[0] != '-') {
if (optstring[0] == '-') {
musl_optarg = argv[musl_optind++];
return 1;
@@ -48,26 +47,28 @@ static int getopt(int argc, char *argv[], char const *optstring) {
return -1;
}
if (!argv[musl_optind][1]) {
if (!argi[1]) {
return -1;
}
if (argv[musl_optind][1] == '-' && !argv[musl_optind][2]) {
return musl_optind++, -1;
if (argi[1] == '-' && !argi[2]) {
++musl_optind;
return -1;
}
if (!musl_optpos) {
++musl_optpos;
}
k = mbtowc(&c, argv[musl_optind] + musl_optpos, MB_LEN_MAX);
wchar_t c;
int k = mbtowc(&c, argi + musl_optpos, MB_LEN_MAX);
if (k < 0) {
k = 1;
c = 0xFFFD; // replacement char
}
optchar = argv[musl_optind] + musl_optpos;
char *optchar = argi + musl_optpos;
musl_optpos += k;
if (!argv[musl_optind][musl_optpos]) {
if (!argi[musl_optpos]) {
++musl_optind;
musl_optpos = 0;
}
@@ -76,8 +77,9 @@ static int getopt(int argc, char *argv[], char const *optstring) {
++optstring;
}
i = 0;
d = 0;
int i = 0;
wchar_t d = 0;
int l;
do {
l = mbtowc(&d, optstring + i, MB_LEN_MAX);
if (l > 0) {
@@ -89,8 +91,8 @@ static int getopt(int argc, char *argv[], char const *optstring) {
if (d != c || c == ':') {
musl_optopt = c;
if (optstring[0] != ':' && musl_opterr) {
musl_getopt_msg(argv[0], ": unrecognized option: ", optchar, k);
if (optstring[0] != ':') {
musl_getopt_msg("unrecognized option: ", optchar);
}
return '?';
}
@@ -105,9 +107,7 @@ static int getopt(int argc, char *argv[], char const *optstring) {
if (optstring[0] == ':') {
return ':';
}
if (musl_opterr) {
musl_getopt_msg(argv[0], ": option requires an argument: ", optchar, k);
}
musl_getopt_msg("option requires an argument: ", optchar);
return '?';
}
}
@@ -116,73 +116,27 @@ static int getopt(int argc, char *argv[], char const *optstring) {
static void permute(char **argv, int dest, int src) {
char *tmp = argv[src];
int i;
for (i = src; i > dest; --i) {
for (int i = src; i > dest; --i) {
argv[i] = argv[i - 1];
}
argv[dest] = tmp;
}
static int musl_getopt_long_core(
int argc, char **argv, char const *optstring, option const *longopts, int *idx, int longonly
);
static int musl_getopt_long(
int argc, char **argv, char const *optstring, option const *longopts, int *idx, int longonly
) {
int ret, skipped, resumed;
if (!musl_optind || musl_optreset) {
musl_optreset = 0;
musl_optpos = 0;
musl_optind = 1;
}
if (musl_optind >= argc || !argv[musl_optind]) {
return -1;
}
skipped = musl_optind;
if (optstring[0] != '+' && optstring[0] != '-') {
int i;
for (i = musl_optind;; ++i) {
if (i >= argc || !argv[i]) {
return -1;
}
if (argv[i][0] == '-' && argv[i][1]) {
break;
}
}
musl_optind = i;
}
resumed = musl_optind;
ret = musl_getopt_long_core(argc, argv, optstring, longopts, idx, longonly);
if (resumed > skipped) {
int i, cnt = musl_optind - resumed;
for (i = 0; i < cnt; ++i) {
permute(argv, skipped, musl_optind - 1);
}
musl_optind = skipped + cnt;
}
return ret;
}
static int musl_getopt_long_core(
int argc, char **argv, char const *optstring, option const *longopts, int *idx, int longonly
) {
static int
musl_getopt_long_core(int argc, char **argv, char const *optstring, option const *longopts) {
musl_optarg = 0;
if (longopts && argv[musl_optind][0] == '-'
&& ((longonly && argv[musl_optind][1] && argv[musl_optind][1] != '-')
|| (argv[musl_optind][1] == '-' && argv[musl_optind][2]))) {
int colon = optstring[optstring[0] == '+' || optstring[0] == '-'] == ':';
int i, cnt, match = 0;
if (char *argi = argv[musl_optind];
!longopts || argi[0] != '-'
|| ((!argi[1] || argi[1] == '-') && (argi[1] != '-' || !argi[2]))) {
return musl_getopt(argc, argv, optstring);
}
bool colon = optstring[optstring[0] == '+' || optstring[0] == '-'] == ':';
int i = 0, cnt = 0, match = 0;
char *arg = 0, *opt, *start = argv[musl_optind] + 1;
for (cnt = i = 0; longopts[i].name; ++i) {
for (; longopts[i].name; ++i) {
char const *name = longopts[i].name;
opt = start;
if (*opt == '-') {
++opt;
@@ -202,12 +156,10 @@ static int musl_getopt_long_core(
}
++cnt;
}
if (cnt == 1 && longonly && arg - start == mblen(start, MB_LEN_MAX)) {
if (cnt == 1 && arg - start == mblen(start, MB_LEN_MAX)) {
int l = arg - start;
for (i = 0; optstring[i]; ++i) {
int j = 0;
while (j < l && start[j] == optstring[i + j]) {
++j;
}
@@ -224,15 +176,10 @@ static int musl_getopt_long_core(
if (*opt == '=') {
if (!longopts[i].has_arg) {
musl_optopt = longopts[i].val;
if (colon || !musl_opterr) {
if (colon) {
return '?';
}
musl_getopt_msg(
argv[0],
": option does not take an argument: ",
longopts[i].name,
strlen(longopts[i].name)
);
musl_getopt_msg("option does not take an argument: ", longopts[i].name);
return '?';
}
musl_optarg = opt + 1;
@@ -243,22 +190,11 @@ static int musl_getopt_long_core(
if (colon) {
return ':';
}
if (!musl_opterr) {
return '?';
}
musl_getopt_msg(
argv[0],
": option requires an argument: ",
longopts[i].name,
strlen(longopts[i].name)
);
musl_getopt_msg("option requires an argument: ", longopts[i].name);
return '?';
}
++musl_optind;
}
if (idx) {
*idx = i;
}
if (longopts[i].flag) {
*longopts[i].flag = longopts[i].val;
return 0;
@@ -267,23 +203,48 @@ static int musl_getopt_long_core(
}
if (argv[musl_optind][1] == '-') {
musl_optopt = 0;
if (!colon && musl_opterr) {
if (!colon) {
musl_getopt_msg(
argv[0],
cnt ? ": option is ambiguous: " : ": unrecognized option: ",
argv[musl_optind] + 2,
strlen(argv[musl_optind] + 2)
cnt ? "option is ambiguous: " : "unrecognized option: ", argv[musl_optind] + 2
);
}
++musl_optind;
return '?';
}
}
return getopt(argc, argv, optstring);
return musl_getopt(argc, argv, optstring);
}
int musl_getopt_long_only(
int argc, char **argv, char const *optstring, option const *longopts, int *idx
) {
return musl_getopt_long(argc, argv, optstring, longopts, idx, 1);
int musl_getopt_long_only(int argc, char **argv, char const *optstring, option const *longopts) {
if (!musl_optind) {
musl_optpos = 0;
musl_optind = 1;
}
if (musl_optind >= argc || !argv[musl_optind]) {
return -1;
}
int skipped = musl_optind;
if (optstring[0] != '+' && optstring[0] != '-') {
int i = musl_optind;
for (;; ++i) {
if (i >= argc || !argv[i]) {
return -1;
}
if (argv[i][0] == '-' && argv[i][1]) {
break;
}
}
musl_optind = i;
}
int resumed = musl_optind;
int ret = musl_getopt_long_core(argc, argv, optstring, longopts);
if (resumed > skipped) {
int cnt = musl_optind - resumed;
for (int i = 0; i < cnt; ++i) {
permute(argv, skipped, musl_optind - 1);
}
musl_optind = skipped + cnt;
}
return ret;
}

View File

@@ -6,6 +6,8 @@
#include "extern/utf8decoder.hpp"
#include <stdint.h>
// clang-format off: vertically align values
static uint8_t const utf8d[] = {
// The first part of the table maps bytes to character classes that

525
src/fix/fix.cpp Normal file
View File

@@ -0,0 +1,525 @@
// SPDX-License-Identifier: MIT
#include "fix/fix.hpp"
#include <sys/stat.h>
#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include "diagnostics.hpp"
#include "helpers.hpp"
#include "platform.hpp"
#include "fix/main.hpp"
#include "fix/mbc.hpp"
#include "fix/warning.hpp"
static constexpr off_t BANK_SIZE = 0x4000;
static ssize_t readBytes(int fd, uint8_t *buf, size_t len) {
// POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results
assume(len <= SSIZE_MAX);
ssize_t total = 0;
while (len) {
ssize_t ret = read(fd, buf, len);
// Return errors, unless we only were interrupted
if (ret == -1 && errno != EINTR) {
return -1; // LCOV_EXCL_LINE
}
// EOF reached
if (ret == 0) {
return total;
}
// If anything was read, accumulate it, and continue
if (ret != -1) {
total += ret;
len -= ret;
buf += ret;
}
}
return total;
}
static ssize_t writeBytes(int fd, uint8_t *buf, size_t len) {
// POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results
assume(len <= SSIZE_MAX);
ssize_t total = 0;
while (len) {
ssize_t ret = write(fd, buf, len);
// Return errors, unless we only were interrupted
if (ret == -1 && errno != EINTR) {
return -1; // LCOV_EXCL_LINE
}
// If anything was written, accumulate it, and continue
if (ret != -1) {
total += ret;
len -= ret;
buf += ret;
}
}
return total;
}
static void overwriteByte(uint8_t *rom0, uint16_t addr, uint8_t fixedByte, char const *areaName) {
uint8_t origByte = rom0[addr];
if (origByte != 0 && origByte != fixedByte) {
warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName);
}
rom0[addr] = fixedByte;
}
static void overwriteBytes(
uint8_t *rom0, uint16_t startAddr, uint8_t const *fixed, uint8_t size, char const *areaName
) {
for (uint8_t i = 0; i < size; ++i) {
uint8_t origByte = rom0[i + startAddr];
if (origByte != 0 && origByte != fixed[i]) {
warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName);
break;
}
}
memcpy(&rom0[startAddr], fixed, size);
}
static void
processFile(int input, int output, char const *name, off_t fileSize, bool expectFileSize) {
if (expectFileSize) {
assume(fileSize != 0);
} else {
assume(fileSize == 0);
}
uint8_t rom0[BANK_SIZE];
ssize_t rom0Len = readBytes(input, rom0, sizeof(rom0));
// Also used as how many bytes to write back when fixing in-place
ssize_t headerSize = (options.cartridgeType & 0xFF00) == TPP1 ? 0x154 : 0x150;
if (rom0Len == -1) {
// LCOV_EXCL_START
error("Failed to read \"%s\"'s header: %s", name, strerror(errno));
return;
// LCOV_EXCL_STOP
} else if (rom0Len < headerSize) {
error(
"\"%s\" too short, expected at least %jd ($%jx) bytes, got only %jd",
name,
static_cast<intmax_t>(headerSize),
static_cast<intmax_t>(headerSize),
static_cast<intmax_t>(rom0Len)
);
return;
}
// Accept partial reads if the file contains at least the header
if (options.fixSpec & (FIX_LOGO | TRASH_LOGO)) {
overwriteBytes(
rom0,
0x0104,
options.logo,
sizeof(options.logo),
options.logoFilename ? "logo" : "Nintendo logo"
);
}
if (options.title) {
overwriteBytes(
rom0,
0x134,
reinterpret_cast<uint8_t const *>(options.title->c_str()),
options.titleLen,
"title"
);
}
if (options.gameID) {
overwriteBytes(
rom0,
0x13F,
reinterpret_cast<uint8_t const *>(options.gameID->c_str()),
options.gameIDLen,
"manufacturer code"
);
}
if (options.model != DMG) {
overwriteByte(rom0, 0x143, options.model == BOTH ? 0x80 : 0xC0, "CGB flag");
}
if (options.newLicensee) {
overwriteBytes(
rom0,
0x144,
reinterpret_cast<uint8_t const *>(options.newLicensee->c_str()),
options.newLicenseeLen,
"new licensee code"
);
}
if (options.sgb) {
overwriteByte(rom0, 0x146, 0x03, "SGB flag");
}
// If a valid MBC was specified...
if (options.cartridgeType < MBC_NONE) {
uint8_t byte = options.cartridgeType;
if ((options.cartridgeType & 0xFF00) == TPP1) {
// Cartridge type isn't directly actionable, translate it
byte = 0xBC;
// The other TPP1 identification bytes will be written below
}
overwriteByte(rom0, 0x147, byte, "cartridge type");
}
// ROM size will be written last, after evaluating the file's size
if ((options.cartridgeType & 0xFF00) == TPP1) {
uint8_t const tpp1Code[2] = {0xC1, 0x65};
overwriteBytes(rom0, 0x149, tpp1Code, sizeof(tpp1Code), "TPP1 identification code");
overwriteBytes(
rom0, 0x150, options.tpp1Rev, sizeof(options.tpp1Rev), "TPP1 revision number"
);
if (options.ramSize != UNSPECIFIED) {
overwriteByte(rom0, 0x152, options.ramSize, "RAM size");
}
overwriteByte(rom0, 0x153, options.cartridgeType & 0xFF, "TPP1 feature flags");
} else {
// Regular mappers
if (options.ramSize != UNSPECIFIED) {
overwriteByte(rom0, 0x149, options.ramSize, "RAM size");
}
if (!options.japanese) {
overwriteByte(rom0, 0x14A, 0x01, "destination code");
}
}
if (options.oldLicensee != UNSPECIFIED) {
overwriteByte(rom0, 0x14B, options.oldLicensee, "old licensee code");
} else if (options.sgb && rom0[0x14B] != 0x33) {
warning(
WARNING_SGB,
"SGB compatibility enabled, but old licensee was 0x%02x, not 0x33",
rom0[0x14B]
);
}
if (options.romVersion != UNSPECIFIED) {
overwriteByte(rom0, 0x14C, options.romVersion, "mask ROM version number");
}
// Remain to be handled the ROM size, and header checksum.
// The latter depends on the former, and so will be handled after it.
// The former requires knowledge of the file's total size, so read that first.
uint16_t globalSum = 0;
// To keep file sizes fairly reasonable, we'll cap the amount of banks at 65536.
// Official mappers only go up to 512 banks, but at least the TPP1 spec allows up to
// 65536 banks = 1 GiB.
// This should be reasonable for the time being, and may be extended later.
std::vector<uint8_t> romx; // Buffer of ROMX bank data
uint32_t nbBanks = 1; // Number of banks *targeted*, including ROM0
size_t totalRomxLen = 0; // *Actual* size of ROMX data
// Handle ROMX
auto errorTooLarge = [&name]() {
error("\"%s\" has more than 65536 banks", name); // LCOV_EXCL_LINE
};
static constexpr off_t NB_BANKS_LIMIT = 0x10000;
static_assert(NB_BANKS_LIMIT * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS");
if (input == output) {
if (fileSize >= NB_BANKS_LIMIT * BANK_SIZE) {
return errorTooLarge(); // LCOV_EXCL_LINE
}
// Compute number of banks and ROMX len from file size
nbBanks = (fileSize + (BANK_SIZE - 1)) / BANK_SIZE; // ceil(fileSize / BANK_SIZE)
totalRomxLen = fileSize >= BANK_SIZE ? fileSize - BANK_SIZE : 0;
} else if (rom0Len == BANK_SIZE) {
// Copy ROMX when reading a pipe, and we're not at EOF yet
for (;;) {
romx.resize(nbBanks * BANK_SIZE);
ssize_t bankLen = readBytes(input, &romx[(nbBanks - 1) * BANK_SIZE], BANK_SIZE);
// Update bank count, ONLY IF at least one byte was read
if (bankLen) {
// We're going to read another bank, check that it won't be too much
if (nbBanks == NB_BANKS_LIMIT) {
return errorTooLarge(); // LCOV_EXCL_LINE
}
++nbBanks;
// Update global checksum, too
for (uint16_t i = 0; i < bankLen; ++i) {
globalSum += romx[totalRomxLen + i];
}
totalRomxLen += bankLen;
}
// Stop when an incomplete bank has been read
if (bankLen != BANK_SIZE) {
break;
}
}
}
// Handle setting the ROM size if padding was requested
// Pad to the next valid power of 2. This is because padding is required by flashers, which
// flash to ROM chips, whose size is always a power of 2... so there'd be no point in
// padding to something else.
// Additionally, a ROM must be at least 32k, so we guarantee a whole amount of banks...
if (options.padValue != UNSPECIFIED) {
// We want at least 2 banks
if (nbBanks == 1) {
if (rom0Len != sizeof(rom0)) {
memset(&rom0[rom0Len], options.padValue, sizeof(rom0) - rom0Len);
// The global checksum hasn't taken ROM0 into consideration yet!
// ROM0 was padded, so treat it as entirely written: update its size
// Update how many bytes were read in total, too
rom0Len = sizeof(rom0);
}
nbBanks = 2;
} else {
assume(rom0Len == sizeof(rom0));
}
assume(nbBanks >= 2);
// Alter number of banks to reflect required value
// x&(x-1) is zero iff x is a power of 2, or 0; we know for sure it's non-zero,
// so this is true (non-zero) when we don't have a power of 2
if (nbBanks & (nbBanks - 1)) {
nbBanks = 1 << (CHAR_BIT * sizeof(nbBanks) - clz(nbBanks));
}
// Write final ROM size
rom0[0x148] = ctz(nbBanks / 2);
// Alter global checksum based on how many bytes will be added (not counting ROM0)
globalSum += options.padValue * ((nbBanks - 1) * BANK_SIZE - totalRomxLen);
}
// Handle the header checksum after the ROM size has been written
if (options.fixSpec & (FIX_HEADER_SUM | TRASH_HEADER_SUM)) {
uint8_t sum = 0;
for (uint16_t i = 0x134; i < 0x14D; ++i) {
sum -= rom0[i] + 1;
}
overwriteByte(
rom0, 0x14D, options.fixSpec & TRASH_HEADER_SUM ? ~sum : sum, "header checksum"
);
}
if (options.fixSpec & (FIX_GLOBAL_SUM | TRASH_GLOBAL_SUM)) {
// Computation of the global checksum does not include the checksum bytes
assume(rom0Len >= 0x14E);
for (uint16_t i = 0; i < 0x14E; ++i) {
globalSum += rom0[i];
}
for (uint16_t i = 0x150; i < rom0Len; ++i) {
globalSum += rom0[i];
}
// Pipes have already read ROMX and updated globalSum, but not regular files
if (input == output) {
for (;;) {
uint8_t bank[BANK_SIZE];
ssize_t bankLen = readBytes(input, bank, sizeof(bank));
for (uint16_t i = 0; i < bankLen; ++i) {
globalSum += bank[i];
}
if (bankLen != sizeof(bank)) {
break;
}
}
}
if (options.fixSpec & TRASH_GLOBAL_SUM) {
globalSum = ~globalSum;
}
uint8_t bytes[2] = {
static_cast<uint8_t>(globalSum >> 8), static_cast<uint8_t>(globalSum & 0xFF)
};
overwriteBytes(rom0, 0x14E, bytes, sizeof(bytes), "global checksum");
}
ssize_t writeLen;
// In case the output depends on the input, reset to the beginning of the file, and only
// write the header
if (input == output) {
if (lseek(output, 0, SEEK_SET) == static_cast<off_t>(-1)) {
// LCOV_EXCL_START
error("Failed to rewind \"%s\": %s", name, strerror(errno));
return;
// LCOV_EXCL_STOP
}
// If modifying the file in-place, we only need to edit the header
// However, padding may have modified ROM0 (added padding), so don't in that case
if (options.padValue == UNSPECIFIED) {
rom0Len = headerSize;
}
}
writeLen = writeBytes(output, rom0, rom0Len);
if (writeLen == -1) {
// LCOV_EXCL_START
error("Failed to write \"%s\"'s ROM0: %s", name, strerror(errno));
return;
// LCOV_EXCL_STOP
} else if (writeLen < rom0Len) {
// LCOV_EXCL_START
error(
"Could only write %jd of \"%s\"'s %jd ROM0 bytes",
static_cast<intmax_t>(writeLen),
name,
static_cast<intmax_t>(rom0Len)
);
return;
// LCOV_EXCL_STOP
}
// Output ROMX if it was buffered
if (!romx.empty()) {
// The value returned is either -1, or smaller than `totalRomxLen`,
// so it's fine to cast to `size_t`
writeLen = writeBytes(output, romx.data(), totalRomxLen);
if (writeLen == -1) {
// LCOV_EXCL_START
error("Failed to write \"%s\"'s ROMX: %s", name, strerror(errno));
return;
// LCOV_EXCL_STOP
} else if (static_cast<size_t>(writeLen) < totalRomxLen) {
// LCOV_EXCL_START
error(
"Could only write %jd of \"%s\"'s %zu ROMX bytes",
static_cast<intmax_t>(writeLen),
name,
totalRomxLen
);
return;
// LCOV_EXCL_STOP
}
}
// Output padding
if (options.padValue != UNSPECIFIED) {
if (input == output) {
if (lseek(output, 0, SEEK_END) == static_cast<off_t>(-1)) {
// LCOV_EXCL_START
error("Failed to seek to end of \"%s\": %s", name, strerror(errno));
return;
// LCOV_EXCL_STOP
}
}
uint8_t bank[BANK_SIZE];
memset(bank, options.padValue, sizeof(bank));
size_t len = (nbBanks - 1) * sizeof(bank) - totalRomxLen; // Don't count ROM0!
while (len) {
static_assert(sizeof(bank) <= SSIZE_MAX, "Bank too large for reading");
size_t thisLen = len > sizeof(bank) ? sizeof(bank) : len;
ssize_t ret = writeBytes(output, bank, thisLen);
// The return value is either -1, or at most `thisLen`,
// so it's fine to cast to `size_t`
if (static_cast<size_t>(ret) != thisLen) {
// LCOV_EXCL_START
error("Failed to write \"%s\"'s padding: %s", name, strerror(errno));
break;
// LCOV_EXCL_STOP
}
len -= thisLen;
}
}
}
bool fix_ProcessFile(char const *name, char const *outputName) {
warnings.nbErrors = 0;
bool inputStdin = !strcmp(name, "-");
if (inputStdin && !outputName) {
outputName = "-";
}
int output = -1;
bool openedOutput = false;
if (outputName) {
if (!strcmp(outputName, "-")) {
output = STDOUT_FILENO;
(void)setmode(STDOUT_FILENO, O_BINARY);
} else {
output = open(outputName, O_WRONLY | O_BINARY | O_CREAT, 0600);
if (output == -1) {
// LCOV_EXCL_START
error("Failed to open \"%s\" for writing: %s", outputName, strerror(errno));
return true;
// LCOV_EXCL_STOP
}
openedOutput = true;
}
}
Defer closeOutput{[&] {
if (openedOutput) {
close(output);
}
}};
if (inputStdin) {
name = "<stdin>";
(void)setmode(STDIN_FILENO, O_BINARY);
processFile(STDIN_FILENO, output, name, 0, false);
} else if (int input = open(name, (outputName ? O_RDONLY : O_RDWR) | O_BINARY); input == -1) {
// POSIX specifies that the results of O_RDWR on a FIFO are undefined.
// However, this is necessary to avoid a TOCTTOU, if the file was changed between
// `stat()` and `open(O_RDWR)`, which could trigger the UB anyway.
// Thus, we're going to hope that either the `open` fails, or it succeeds but I/O
// operations may fail, all of which we handle.
error("Failed to open \"%s\" for reading+writing: %s", name, strerror(errno));
} else {
Defer closeInput{[&] { close(input); }};
struct stat stat;
if (fstat(input, &stat) == -1) {
error("Failed to stat \"%s\": %s", name, strerror(errno)); // LCOV_EXCL_LINE
} else if (!S_ISREG(stat.st_mode)) { // We do not support FIFOs or symlinks
// LCOV_EXCL_START
error("\"%s\" is not a regular file, and thus cannot be modified in-place", name);
// LCOV_EXCL_STOP
} else if (stat.st_size < 0x150) {
// This check is in theory redundant with the one in `processFile`, but it
// prevents passing a file size of 0, which usually indicates pipes
error(
"\"%s\" too short, expected at least 336 ($150) bytes, got only %jd",
name,
static_cast<intmax_t>(stat.st_size)
);
} else {
if (!outputName) {
output = input;
}
processFile(input, output, name, stat.st_size, true);
}
}
return checkErrors(name);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,18 @@
// SPDX-License-Identifier: MIT
#include "fix/mbc.hpp"
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unordered_map>
#include <utility>
#include "helpers.hpp" // unreachable_
#include "platform.hpp" // strcasecmp
#include "util.hpp"
#include "fix/warning.hpp"
@@ -91,87 +98,57 @@ bool mbc_HasRAM(MbcType type) {
return search != mbcData.end() && search->second.second;
}
static void skipWhitespace(char const *&ptr) {
while (*ptr == ' ' || *ptr == '\t') {
++ptr;
}
}
static void skipMBCSpace(char const *&ptr) {
while (*ptr == ' ' || *ptr == '\t' || *ptr == '_') {
++ptr;
}
ptr += strspn(ptr, " \t_");
}
static char normalizeMBCChar(char c) {
if (c >= 'a' && c <= 'z') { // Uppercase for comparison with `mbc_Name`s
c = c - 'a' + 'A';
} else if (c == '_') { // Treat underscores as spaces
c = ' ';
if (c == '_') {
return ' '; // Treat underscores as spaces
}
return c;
}
static bool readMBCSlice(char const *&name, char const *expected) {
while (*expected) {
// If `name` is too short, the character will be '\0' and this will return `false`
if (normalizeMBCChar(*name++) != *expected++) {
return false;
}
}
return true;
return toUpper(c); // Uppercase for comparison with `mbc_Name`s
}
[[noreturn]]
static void fatalUnknownMBC(char const *fullName) {
fatal("Unknown MBC \"%s\"\n%s", fullName, acceptedMBCNames);
static void fatalUnknownMBC(char const *name) {
fatal("Unknown MBC \"%s\"\n%s", name, acceptedMBCNames);
}
[[noreturn]]
static void fatalWrongMBCFeatures(char const *fullName) {
fatal("Features incompatible with MBC (\"%s\")\n%s", fullName, acceptedMBCNames);
static void fatalWrongMBCFeatures(char const *name) {
fatal("Features incompatible with MBC (\"%s\")\n%s", name, acceptedMBCNames);
}
MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor) {
char const *fullName = name;
char const *ptr = name + strspn(name, " \t"); // Skip leading blank space
if (!strcasecmp(name, "help") || !strcasecmp(name, "list")) {
if (!strcasecmp(ptr, "help") || !strcasecmp(ptr, "list")) {
puts(acceptedMBCNames); // Outputs to stdout and appends a newline
exit(0);
}
if ((name[0] >= '0' && name[0] <= '9') || name[0] == '$') {
int base = 0;
if (name[0] == '$') {
++name;
base = 16;
// Parse numeric MBC and return it as-is (unless it's too large)
if (char c = *ptr; isDigit(c) || c == '$' || c == '&' || c == '%') {
if (std::optional<uint64_t> mbc = parseWholeNumber(ptr); !mbc) {
fatalUnknownMBC(name);
} else if (*mbc > 0xFF) {
fatal("Specified MBC ID out of range 0-255: \"%s\"", name);
} else {
return static_cast<MbcType>(*mbc);
}
// Parse number, and return it as-is (unless it's too large)
char *endptr;
unsigned long mbc = strtoul(name, &endptr, base);
if (*endptr) {
fatalUnknownMBC(fullName);
}
if (mbc > 0xFF) {
fatal("Specified MBC ID out of range 0-255: %s", fullName);
}
return static_cast<MbcType>(mbc);
}
// Begin by reading the MBC type:
uint16_t mbc;
char const *ptr = name;
uint16_t mbc = UINT16_MAX;
skipWhitespace(ptr); // Trim off leading whitespace
#define tryReadSlice(expected) \
do { \
if (!readMBCSlice(ptr, expected)) { \
fatalUnknownMBC(fullName); \
} \
} while (0)
auto tryReadSlice = [&ptr, &name](char const *expected) {
while (*expected) {
// If `name` is too short, the character will be '\0' and this will return `false`
if (normalizeMBCChar(*ptr++) != *expected++) {
fatalUnknownMBC(name);
}
}
};
switch (*ptr++) {
case 'R': // ROM / ROM_ONLY
@@ -191,13 +168,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
switch (*ptr++) {
case 'B':
case 'b':
switch (*ptr++) {
case 'C':
case 'c':
break;
default:
fatalUnknownMBC(fullName);
}
tryReadSlice("C");
switch (*ptr++) {
case '1':
mbc = MBC1;
@@ -217,8 +188,6 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
case '7':
mbc = MBC7_SENSOR_RUMBLE_RAM_BATTERY;
break;
default:
fatalUnknownMBC(fullName);
}
break;
case 'M':
@@ -226,8 +195,6 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
tryReadSlice("M01");
mbc = MMM01;
break;
default:
fatalUnknownMBC(fullName);
}
break;
@@ -250,39 +217,30 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
tryReadSlice("MA5");
mbc = BANDAI_TAMA5;
break;
case 'P': {
case 'P':
tryReadSlice("P1");
// Parse version
skipMBCSpace(ptr);
// Major
char *endptr;
unsigned long val = strtoul(ptr, &endptr, 10);
if (endptr == ptr) {
if (std::optional<uint64_t> major = parseNumber(ptr, BASE_10); !major) {
fatal("Failed to parse TPP1 major revision number");
}
ptr = endptr;
if (val != 1) {
} else if (*major != 1) {
fatal("RGBFIX only supports TPP1 version 1.0");
} else {
tpp1Major = *major;
}
tpp1Major = val;
tryReadSlice(".");
// Minor
val = strtoul(ptr, &endptr, 10);
if (endptr == ptr) {
if (std::optional<uint64_t> minor = parseNumber(ptr, BASE_10); !minor) {
fatal("Failed to parse TPP1 minor revision number");
}
ptr = endptr;
if (val > 0xFF) {
} else if (*minor > 0xFF) {
fatal("TPP1 minor revision number must be 8-bit");
} else {
tpp1Minor = *minor;
}
tpp1Minor = val;
mbc = TPP1;
break;
}
default:
fatalUnknownMBC(fullName);
}
break;
case 'H': // HuC{1, 3}
@@ -295,13 +253,12 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
case '3':
mbc = HUC3;
break;
default:
fatalUnknownMBC(fullName);
}
break;
}
default:
fatalUnknownMBC(fullName);
if (mbc == UINT16_MAX) {
fatalUnknownMBC(name);
}
// Read "additional features"
@@ -315,18 +272,10 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
static constexpr uint8_t MULTIRUMBLE = 1 << 2;
// clang-format on
for (;;) {
skipWhitespace(ptr); // Trim off trailing whitespace
// If done, start processing "features"
if (!*ptr) {
break;
}
while (*ptr) {
// We expect a '+' at this point
skipMBCSpace(ptr);
if (*ptr++ != '+') {
fatalUnknownMBC(fullName);
}
tryReadSlice("+");
skipMBCSpace(ptr);
switch (*ptr++) {
@@ -355,8 +304,6 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
tryReadSlice("M");
features |= RAM;
break;
default:
fatalUnknownMBC(fullName);
}
break;
@@ -371,12 +318,8 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
tryReadSlice("IMER");
features |= TIMER;
break;
default:
fatalUnknownMBC(fullName);
}
}
#undef tryReadSlice
switch (mbc) {
case ROM:
@@ -397,7 +340,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
} else if (features == (RAM | BATTERY)) {
mbc += 2;
} else if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
@@ -405,7 +348,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
if (features == BATTERY) {
mbc = MBC2_BATTERY;
} else if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
@@ -413,7 +356,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
// Handle timer, which also requires battery
if (features & TIMER) {
if (!(features & BATTERY)) {
warning(WARNING_MBC, "MBC3+TIMER implies BATTERY");
warning(WARNING_MBC, "\"MBC3+TIMER\" implies \"BATTERY\"");
}
features &= ~(TIMER | BATTERY); // Reset those bits
mbc = MBC3_TIMER_BATTERY;
@@ -429,7 +372,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
} else if (features == (RAM | BATTERY)) {
mbc += 2;
} else if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
@@ -447,7 +390,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
} else if (features == (RAM | BATTERY)) {
mbc += 2;
} else if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
@@ -457,53 +400,50 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
case HUC3:
// No extra features accepted
if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
if (features != (SENSOR | RUMBLE | RAM | BATTERY)) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
case HUC1_RAM_BATTERY:
if (features != (RAM | BATTERY)) { // HuC1 expects RAM+BATTERY
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
case TPP1:
case TPP1: {
// clang-format off: vertically align values
static constexpr uint8_t BATTERY_TPP1 = 1 << 3;
static constexpr uint8_t TIMER_TPP1 = 1 << 2;
static constexpr uint8_t MULTIRUMBLE_TPP1 = 1 << 1;
static constexpr uint8_t RUMBLE_TPP1 = 1 << 0;
// clang-format on
if (features & RAM) {
warning(WARNING_MBC, "TPP1 requests RAM implicitly if given a non-zero RAM size");
}
if (features & BATTERY) {
mbc |= 0x08;
mbc |= BATTERY_TPP1;
}
if (features & TIMER) {
mbc |= 0x04;
}
if (features & MULTIRUMBLE) {
mbc |= 0x03; // Also set the rumble flag
mbc |= TIMER_TPP1;
}
if (features & RUMBLE) {
mbc |= 0x01;
mbc |= RUMBLE_TPP1;
}
if (features & SENSOR) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
// Multiple rumble speeds imply rumble
if (mbc & 0x01) {
assume(mbc & 0x02);
if (features & MULTIRUMBLE) {
mbc |= MULTIRUMBLE_TPP1 | RUMBLE_TPP1; // Multiple rumble speeds imply rumble
}
break;
}
skipWhitespace(ptr); // Trim off trailing whitespace
// If there is still something past the whitespace, error out
if (*ptr) {
fatalUnknownMBC(fullName);
}
return static_cast<MbcType>(mbc);

View File

@@ -1,5 +1,16 @@
// SPDX-License-Identifier: MIT
#include "fix/warning.hpp"
#include <inttypes.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "diagnostics.hpp"
#include "style.hpp"
// clang-format off: nested initializers
Diagnostics<WarningLevel, WarningID> warnings = {
.metaWarnings = {
@@ -8,6 +19,7 @@ Diagnostics<WarningLevel, WarningID> warnings = {
},
.warningFlags = {
{"mbc", LEVEL_DEFAULT },
{"obsolete", LEVEL_DEFAULT },
{"overwrite", LEVEL_DEFAULT },
{"sgb", LEVEL_DEFAULT },
{"truncation", LEVEL_DEFAULT },
@@ -20,6 +32,7 @@ Diagnostics<WarningLevel, WarningID> warnings = {
uint32_t checkErrors(char const *filename) {
if (warnings.nbErrors > 0) {
style_Set(stderr, STYLE_RED, true);
fprintf(
stderr,
"Fixing \"%s\" failed with %" PRIu64 " error%s\n",
@@ -27,13 +40,16 @@ uint32_t checkErrors(char const *filename) {
warnings.nbErrors,
warnings.nbErrors == 1 ? "" : "s"
);
style_Reset(stderr);
}
return warnings.nbErrors;
}
void error(char const *fmt, ...) {
va_list ap;
style_Set(stderr, STYLE_RED, true);
fputs("error: ", stderr);
style_Reset(stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
@@ -44,7 +60,9 @@ void error(char const *fmt, ...) {
void fatal(char const *fmt, ...) {
va_list ap;
style_Set(stderr, STYLE_RED, true);
fputs("FATAL: ", stderr);
style_Reset(stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
@@ -62,19 +80,27 @@ void warning(WarningID id, char const *fmt, ...) {
break;
case WarningBehavior::ENABLED:
fprintf(stderr, "warning: [-W%s]\n ", flag);
style_Set(stderr, STYLE_YELLOW, true);
fputs("warning: ", stderr);
style_Reset(stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
style_Set(stderr, STYLE_YELLOW, true);
fprintf(stderr, " [-W%s]\n", flag);
style_Reset(stderr);
break;
case WarningBehavior::ERROR:
fprintf(stderr, "error: [-Werror=%s]\n ", flag);
style_Set(stderr, STYLE_RED, true);
fputs("error: ", stderr);
style_Reset(stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
style_Set(stderr, STYLE_RED, true);
fprintf(stderr, " [-Werror=%s]\n", flag);
style_Reset(stderr);
warnings.incrementErrors();
break;

View File

@@ -3,6 +3,10 @@
#include "gfx/color_set.hpp"
#include <algorithm>
#include <iterator>
#include <stdint.h>
#include <stdlib.h>
#include <utility>
#include "helpers.hpp"

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More