Compare commits

...

78 Commits

Author SHA1 Message Date
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
577 changed files with 8251 additions and 6127 deletions

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 \

69
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,69 @@
# RGBDS Architecture
## 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
```
- `.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).
- `CMakeLists.txt` - defines how to build RGBDS with CMake.
- `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.
- `Makefile` - defines how to build RGBDS with `make`.

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-rc1
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
@@ -52,7 +52,9 @@ all: rgbasm rgblink rgbfix rgbgfx
common_obj := \
src/extern/getopt.o \
src/diagnostics.o \
src/usage.o
src/style.o \
src/usage.o \
src/util.o
rgbasm_obj := \
${common_obj} \
@@ -72,15 +74,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 +97,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
@@ -117,7 +123,7 @@ rgbgfx_obj := \
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 +239,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 'include/**/*.hpp' 'src/**/*.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 'include/**/*.hpp' 'src/**/*.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

@@ -14,11 +14,11 @@ 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
MissingInclude missingIncludeState = INC_ERROR; // -MC, -MG
@@ -42,11 +42,4 @@ struct Options {
extern Options options;
#define verbosePrint(...) \
do { \
if (options.verbose) { \
fprintf(stderr, __VA_ARGS__); \
} \
} while (0)
#endif // RGBDS_ASM_MAIN_HPP

View File

@@ -6,7 +6,6 @@
#include <memory>
#include <stdint.h>
#include <string>
#include <unordered_map>
#include <vector>
#include "linkdefs.hpp"

View File

@@ -40,7 +40,7 @@ struct Expression {
void makeUnaryOp(RPNCommand op, Expression &&src);
void makeBinaryOp(RPNCommand op, Expression &&src1, Expression const &src2);
bool makeCheckHRAM();
void makeCheckHRAM();
void makeCheckRST();
void makeCheckBitIndex(uint8_t mask);

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();

85
include/backtrace.hpp Normal file
View File

@@ -0,0 +1,85 @@
// 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"
struct Tracing {
uint64_t depth = 0;
bool collapse = false;
bool loud = false;
};
extern Tracing tracing;
bool trace_ParseTraceDepth(char const *arg);
template<typename T, typename M, typename N>
void trace_PrintBacktrace(std::vector<T> const &stack, M getName, N getLineNo) {
size_t n = stack.size();
if (n == 0) {
return; // LCOV_EXCL_LINE
}
auto printLocation = [&](size_t i) {
T 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" : "<-");
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(" <-", 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

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"
@@ -68,7 +68,7 @@ struct Diagnostics {
}
WarningBehavior getWarningBehavior(W id) const;
std::string processWarningFlag(char const *flag);
void processWarningFlag(char const *flag);
};
template<typename L, typename W>
@@ -121,18 +121,18 @@ WarningBehavior Diagnostics<L, W>::getWarningBehavior(W id) const {
}
template<typename L, typename W>
std::string Diagnostics<L, W>::processWarningFlag(char const *flag) {
void Diagnostics<L, W>::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);
@@ -174,12 +174,12 @@ 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
@@ -194,19 +194,18 @@ std::string Diagnostics<L, W>::processWarningFlag(char const *flag) {
state.metaStates[id].update(flagState);
}
}
return rootFlag;
return;
}
// Try to match against a "normal" flag
for (W id : EnumSeq(W::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

@@ -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

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

@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_FIX_MAIN_HPP
#define RGBDS_FIX_MAIN_HPP
#include <stdint.h>
#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
char const *gameID = nullptr; // -i
uint8_t gameIDLen;
char const *newLicensee = nullptr; // -k
uint8_t newLicenseeLen;
char const *logoFilename = nullptr; // -L
uint8_t logo[48] = {};
MbcType cartridgeType = MBC_NONE; // -m
uint8_t tpp1Rev[2];
char const *title = nullptr; // -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

View File

@@ -5,9 +5,9 @@
#include <array>
#include <optional>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <utility>
#include <vector>
#include "helpers.hpp"
@@ -20,7 +20,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
@@ -42,6 +41,7 @@ struct Options {
uint16_t height;
uint32_t right() const { return left + width * 8; }
uint32_t bottom() const { return top + height * 8; }
bool specified() const { return left || top || width || height; }
} inputSlice{0, 0, 0, 0}; // -L (margins in clockwise order, like CSS)
uint8_t basePalID = 0; // -l
std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N
@@ -56,18 +56,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; }

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);

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

@@ -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

@@ -3,9 +3,57 @@
#ifndef RGBDS_ITERTOOLS_HPP
#define RGBDS_ITERTOOLS_HPP
#include <deque>
#include <optional>
#include <stddef.h>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>
template<typename T>
class InsertionOrderedMap {
std::deque<T> list;
std::unordered_map<std::string, size_t> map; // Indexes into `list`
public:
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(); }
T &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(); }
T &add(std::string const &name) {
map[name] = list.size();
return list.emplace_back();
}
T &add(std::string const &name, T &&value) {
map[name] = list.size();
list.emplace_back(std::move(value));
return list.back();
}
T &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;
}
};
template<typename T>
class EnumSeq {
T _start;

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,11 +3,9 @@
#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();

View File

@@ -4,12 +4,6 @@
#define RGBDS_LINK_MAIN_HPP
#include <stdint.h>
#include <stdio.h>
#include <string>
#include <variant>
#include <vector>
#include "linkdefs.hpp"
struct Options {
bool isDmgMode; // -d
@@ -25,41 +19,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,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;

View File

@@ -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

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,20 +3,44 @@
#ifndef RGBDS_UTIL_HPP
#define RGBDS_UTIL_HPP
#include <algorithm>
#include <ctype.h>
#include <numeric>
#include <stddef.h>
#include <string>
#include <unordered_map>
#include "helpers.hpp"
bool isNewline(int c);
bool isBlankSpace(int c);
bool isWhitespace(int c);
bool isPrintable(int c);
bool isLetter(int c);
bool isDigit(int c);
bool isOctDigit(int c);
bool isHexDigit(int c);
bool isAlphanumeric(int c);
bool startsIdentifier(int c);
bool continuesIdentifier(int c);
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>

33
include/verbosity.hpp Normal file
View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_VERBOSITY_HPP
#define RGBDS_VERBOSITY_HPP
#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 1
char const *get_package_version_string();

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 1, 2025
.Dt GBZ80 7
.Os
.Sh NAME
@@ -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 1, 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 .
.Fl \-include .
.Ss rgbfix -O/--overwrite
Deprecated in 1.0.0.
.Pp
Instead, use
.Dl -Wno-overwrite .
.Ss rgbgfx -h
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

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 1, 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
@@ -51,6 +53,29 @@ is invalid because it could also be
.Fl \-version .
The arguments are as follows:
.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 +90,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 +270,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 .
@@ -339,10 +394,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
@@ -461,7 +518,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 1, 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
@@ -615,7 +624,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,18 +634,6 @@ 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.
@@ -1691,7 +1688,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
@@ -1720,8 +1718,6 @@ The following symbols are defined by the assembler:
.It Dv .. Ta Ic EQUS Ta The current local label scope
.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 +2490,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 +2524,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 1, 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

View File

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

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 31, 2025
.Dd September 1, 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
@@ -40,19 +41,19 @@ and to have already populated whichever fields they don't specify using
.Nm .
.Pp
The input
.Ar asmfile
.Ar file
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
.Fl \-verb
is
.Fl \-color-only ,
.Fl \-verbose ,
but
.Fl \-color
.Fl \-ver
is invalid because it could also be
.Fl \-color-compatible .
.Fl \-version .
Options later in the command line override those set earlier.
Accepted options are as follows:
.Bl -tag -width Ds
@@ -70,6 +71,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 +149,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 +169,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
@@ -240,6 +250,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 +327,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 1, 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
@@ -177,8 +178,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 +213,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).
@@ -382,14 +412,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
@@ -600,9 +633,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
@@ -777,6 +811,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 +860,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 1, 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
@@ -48,7 +50,7 @@ option, which implies
but also prohibits the use of banked VRAM.
.Pp
The input
.Ar asmfile
.Ar file
can be a path to a file, or
.Cm \-
to read from standard input.
@@ -63,6 +65,41 @@ is invalid because it could also be
.Fl \-version .
The arguments are as follows:
.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 +151,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 .
@@ -137,8 +192,13 @@ When making a ROM, note that not using this is not a replacement for
option!
.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
@@ -258,11 +318,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
warns when an N-bit value is 2**N or greater, or less than -2**N.
.Fl Wtruncation=2
or just
.Fl Wtruncation
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 +351,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 1, 2025
.Dt RGBLINK 5
.Os
.Sh NAME

View File

@@ -5,7 +5,9 @@ configure_file(version.cpp _version.cpp ESCAPE_QUOTES)
set(common_src
"extern/getopt.cpp"
"diagnostics.cpp"
"style.cpp"
"usage.cpp"
"util.cpp"
"_version.cpp"
)
@@ -50,14 +52,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 +73,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"
@@ -91,7 +97,7 @@ set(rgbgfx_src
"gfx/reverse.cpp"
"gfx/rgba.cpp"
"gfx/warning.cpp"
"util.cpp"
"verbosity.cpp"
)
foreach(PROG "asm" "fix" "gfx" "link")

View File

@@ -2,19 +2,134 @@
#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");
}
lexer_SetMode(LEXER_SKIP_TO_ENDC);
} 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()) {
fatal("Found `ELSE` after an `ELSE` block");
}
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 +153,117 @@ 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");
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 +302,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 +371,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 +445,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 +469,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 +522,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 +551,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 +588,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"
@@ -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,76 @@
#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" // isDigit
#include "asm/fixpoint.hpp"
#include "asm/main.hpp" // options
#include "asm/warning.hpp"
void FormatSpec::useCharacter(int c) {
if (state == FORMAT_INVALID) {
return;
static size_t parseNumber(char const *spec, size_t &value) {
size_t i = 0;
value = 0;
for (; isDigit(spec[i]); ++i) {
value = value * 10 + (spec[i] - '0');
}
switch (c) {
// sign
case ' ':
case '+':
if (state > FORMAT_SIGN) {
break;
return i;
}
state = FORMAT_EXACT;
size_t FormatSpec::parseSpec(char const *spec) {
size_t i = 0;
// <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;
alignLeft = true;
return;
// pad, width, and prec values
case '0':
if (state < FORMAT_WIDTH) {
// <align>
if (spec[i] == '-') {
++i;
alignLeft = true;
}
// <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;
}
return;
// width
case '.':
if (state > FORMAT_WIDTH) {
break;
// <width>
if (isDigit(spec[i])) {
i += parseNumber(&spec[i], width);
}
state = FORMAT_FRAC;
// <frac>
if (spec[i] == '.') {
++i;
hasFrac = true;
return;
// prec
case 'q':
if (state > FORMAT_PREC) {
break;
i += parseNumber(&spec[i], fracWidth);
}
state = FORMAT_PREC;
hasPrec = true;
return;
// type
// <prec>
if (spec[i] == 'q') {
++i;
hasPrec = true;
i += parseNumber(&spec[i], precision);
}
// <type>
switch (char c = spec[i]; c) {
case 'd':
case 'u':
case 'X':
@@ -103,26 +83,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;
}
parsed = true;
return i;
}
static std::string escapeString(std::string const &str) {
@@ -157,7 +124,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 +163,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;

View File

@@ -7,14 +7,22 @@
#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 "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,9 +56,9 @@ static std::vector<std::string> includePaths = {""}; // -I
static std::deque<std::string> preIncludeNames; // -P
static bool failedOnMissingInclude = false;
std::string FileStackNode::reptChain() const {
static std::string reptChain(FileStackNode const &node) {
std::string chain;
std::vector<uint32_t> const &nodeIters = iters();
std::vector<uint32_t> const &nodeIters = node.iters();
for (uint32_t i = nodeIters.size(); i--;) {
chain.append("::REPT~");
chain.append(std::to_string(nodeIters[i]));
@@ -58,35 +66,73 @@ std::string FileStackNode::reptChain() const {
return chain;
}
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;
} else {
if (parent) {
parent->dump(lineNo);
fputs(" -> ", stderr);
}
std::string const &nodeName = name();
fputs(nodeName.c_str(), stderr);
fprintf(stderr, "(%" PRIu32 ")", curLineNo);
return nodeName;
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 {}; // LCOV_EXCL_LINE
}
bool fstk_DumpCurrent() {
if (lexer_AtTopLevel()) {
return false;
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
traceNodes.emplace_back(traceNodes.back().first + reptChain(node), curLineNo);
} else {
traceNodes.emplace_back(node.name(), curLineNo);
}
return traceNodes;
}
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 +170,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 +204,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 +233,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 +260,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 +285,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 +299,12 @@ static void newMacroContext(Symbol const &macro, std::shared_ptr<MacroArgs> macr
}
}
if (macro.src->type == NODE_REPT) {
fileInfoName.append(macro.src->reptChain());
fileInfoName.append(reptChain(*macro.src));
}
fileInfoName.append("::");
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,68 @@ 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) {
static char const *suggestDef(std::shared_ptr<MacroArgs> const macroArgs) {
std::shared_ptr<std::string> arg = macroArgs->getArg(1);
if (!arg) {
return nullptr;
}
char const *str = arg->c_str();
static char const *types[] = {"EQUS", "EQU", "RB", "RW", "RL", "="};
for (size_t i = 0; i < std::size(types); ++i) {
if (char const *type = types[i]; strncasecmp(str, type, strlen(type)) == 0) {
return type;
}
}
if (strncasecmp(str, "SET", literal_strlen("SET")) == 0) {
return "=";
}
return nullptr;
}
void fstk_RunMacro(
std::string const &macroName, std::shared_ptr<MacroArgs> macroArgs, bool isQuiet
) {
Symbol *macro = sym_FindExactSymbol(macroName);
if (!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 (char const *defType = suggestDef(macroArgs); defType) {
error(
"Undefined macro `%s` (did you mean \"DEF %s %s ...\"?)",
macroName.c_str(),
macroName.c_str(),
defType
);
} else {
error("Macro \"%s\" not defined", macroName.c_str());
error("Undefined macro `%s`", macroName.c_str());
}
return;
}
if (macro->type != SYM_MACRO) {
error("\"%s\" is not a macro", macroName.c_str());
error("`%s` is not a macro", macroName.c_str());
return;
}
newMacroContext(*macro, macroArgs);
newMacroContext(*macro, macroArgs, isQuiet || macro->isQuiet);
}
void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span) {
void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span, bool isQuiet) {
if (count == 0) {
return;
}
newReptContext(reptLineNo, span, count);
newReptContext(reptLineNo, span, count, isQuiet);
}
void fstk_RunFor(
@@ -363,7 +444,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 +457,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 +479,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 +495,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,235 @@
#include <algorithm>
#include <errno.h>
#include <limits.h>
#include <inttypes.h>
#include <memory>
#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 "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) {
static char const *dependFileName = nullptr; // -M
static std::unordered_map<std::string, std::vector<StateFeature>> stateFileSpecs; // -s
// Short options
static char const *optstring = "B:b:D:Eg:hI:M:o:P:p:Q:r:s:VvW:wX:";
// Variables for the long-only options
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
// LCOV_EXCL_START
static void verboseOutputConfig(int argc, char *argv[]) {
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 (!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] : 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 (musl_optind < argc) {
fprintf(stderr, "\tInput asm file: %s", argv[musl_optind]);
if (musl_optind + 1 < argc) {
fprintf(stderr, " (and %d more)", argc - musl_optind - 1);
}
putc('\n', stderr);
}
// -o/--output
if (!options.objectFileName.empty()) {
fprintf(stderr, "\tOutput object file: %s\n", options.objectFileName.c_str());
}
fstk_VerboseOutputConfig();
if (dependFileName) {
fprintf(
stderr,
"\tOutput dependency file: %s\n",
strcmp(dependFileName, "-") ? dependFileName : "<stdout>"
);
// -MT or -MQ
if (!options.targetFileName.empty()) {
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
static std::string escapeMakeChars(std::string &str) {
std::string escaped;
size_t pos = 0;
for (;;) {
@@ -46,65 +249,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 +258,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 +266,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 +278,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);
}
@@ -164,25 +308,25 @@ int main(int argc, char *argv[]) {
options.maxErrors = 100;
}
// Local options
char const *dependFileName = nullptr; // -M
std::unordered_map<std::string, std::vector<StateFeature>> stateFileSpecs; // -s
// Parse CLI options
for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) {
switch (ch) {
char *endptr;
case 'B':
if (!trace_ParseTraceDepth(musl_optarg)) {
fatal("Invalid argument for option '-B'");
}
break;
case 'b':
if (strlen(musl_optarg) == 2) {
opt_B(musl_optarg);
} 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(musl_optarg, '=');
if (equals) {
*equals = '\0';
sym_AddString(musl_optarg, std::make_shared<std::string>(equals + 1));
@@ -190,123 +334,120 @@ int main(int argc, char *argv[]) {
sym_AddString(musl_optarg, 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);
} 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);
break;
case 'M':
if (options.dependFile) {
warnx("Overriding dependfile %s", dependFileName);
if (dependFileName) {
warnx(
"Overriding dependency file \"%s\"",
strcmp(dependFileName, "-") ? dependFileName : "<stdout>"
);
}
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
}
break;
case 'o':
if (!options.objectFileName.empty()) {
warnx("Overriding output filename %s", options.objectFileName.c_str());
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
break;
case 'P':
fstk_AddPreIncludeFile(musl_optarg);
break;
unsigned long padByte;
case 'p':
padByte = strtoul(musl_optarg, &endptr, 0);
case 'p': {
char *endptr;
unsigned long padByte = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'p'");
fatal("Invalid argument for option '-p'");
}
if (padByte > 0xFF) {
fatal("Argument for option 'p' must be between 0 and 0xFF");
fatal("Argument for option '-p' must be between 0 and 0xFF");
}
opt_P(padByte);
break;
}
case 'Q': {
char const *precisionArg = musl_optarg;
if (precisionArg[0] == '.') {
++precisionArg;
}
char *endptr;
unsigned long precision = strtoul(precisionArg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'Q'");
fatal("Invalid argument for option '-Q'");
}
if (precision < 1 || precision > 31) {
fatal("Argument for option 'Q' must be between 1 and 31");
fatal("Argument for option '-Q' must be between 1 and 31");
}
opt_Q(precision);
break;
}
case 'r':
case 'r': {
char *endptr;
options.maxRecursionDepth = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'r'");
fatal("Invalid argument for option '-r'");
}
break;
}
case 's': {
// Split "<features>:<name>" so `musl_optarg` is "<features>" and `name` is "<name>"
char *name = strchr(musl_optarg, ':');
if (!name) {
fatal("Invalid argument for option 's'");
fatal("Invalid argument for option '-s'");
}
*name++ = '\0';
std::vector<StateFeature> features = parseStateFeatures(musl_optarg);
if (stateFileSpecs.find(name) != stateFileSpecs.end()) {
warnx("Overriding state filename %s", name);
warnx("Overriding state file \"%s\"", name);
}
verbosePrint("State filename %s\n", name); // LCOV_EXCL_LINE
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
@@ -319,23 +460,29 @@ int main(int argc, char *argv[]) {
break;
case 'X': {
char *endptr;
uint64_t maxErrors = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
fatal("Invalid argument for option 'X'");
fatal("Invalid argument for option '-X'");
}
if (maxErrors > UINT64_MAX) {
fatal("Argument for option 'X' must be between 0 and %" PRIu64, 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 0: // Long-only options
switch (longOpt) {
case 'c':
if (!style_Parse(musl_optarg)) {
fatal("Invalid argument for option '--color'");
}
break;
case 'C':
options.missingIncludeState = GEN_CONTINUE;
break;
@@ -351,8 +498,8 @@ int main(int argc, char *argv[]) {
case 'Q':
case 'T': {
std::string newTarget = musl_optarg;
if (depType == 'Q') {
newTarget = make_escape(newTarget);
if (longOpt == 'Q') {
newTarget = escapeMakeChars(newTarget);
}
if (!options.targetFileName.empty()) {
options.targetFileName += ' ';
@@ -373,19 +520,34 @@ int main(int argc, char *argv[]) {
options.targetFileName = options.objectFileName;
}
verboseOutputConfig(argc, argv);
if (argc == musl_optind) {
usage.printAndExit("Please specify an input file (pass `-` to read from standard input)");
usage.printAndExit("No input file specified (pass \"-\" to read from standard input)");
} else if (argc != musl_optind + 1) {
usage.printAndExit("More than one input file specified");
}
std::string mainFileName = argv[musl_optind];
verbosePrint("Assembling %s\n", mainFileName.c_str()); // LCOV_EXCL_LINE
verbosePrint(VERB_NOTICE, "Assembling \"%s\"\n", mainFileName.c_str()); // LCOV_EXCL_LINE
if (dependFileName) {
if (strcmp("-", dependFileName)) {
options.dependFile = fopen(dependFileName, "w");
if (options.dependFile == nullptr) {
// LCOV_EXCL_START
fatal("Failed to open dependency file \"%s\": %s", dependFileName, strerror(errno));
// LCOV_EXCL_STOP
}
} else {
options.dependFile = stdout;
}
}
if (options.dependFile && options.targetFileName.empty()) {
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);
@@ -420,7 +582,7 @@ int main(int argc, char *argv[]) {
out_WriteObject();
for (auto [name, features] : stateFileSpecs) {
for (auto const &[name, features] : 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 <stack>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "diagnostics.hpp"
#include "helpers.hpp" // assume
#include "util.hpp" // isBlankSpace
#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,12 +50,13 @@ 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) {
if (s[0] == '-') {
++s;
}
switch (s[0]) {
case 'b':
if (strlen(&s[1]) == 2) {
@@ -90,9 +92,8 @@ void opt_Parse(char const *s) {
}
break;
char const *precisionArg;
case 'Q':
precisionArg = &s[1];
case 'Q': {
char const *precisionArg = &s[1];
if (precisionArg[0] == '.') {
++precisionArg;
}
@@ -112,15 +113,16 @@ void opt_Parse(char const *s) {
error("Invalid argument for option 'Q'");
}
break;
}
case 'r': {
++s; // Skip 'r'
while (isblank(*s)) {
++s; // Skip leading whitespace
while (isBlankSpace(*s)) {
++s; // Skip leading blank spaces
}
if (s[0] == '\0') {
error("Missing argument to option 'r'");
error("Missing argument for option 'r'");
break;
}
@@ -128,9 +130,9 @@ void opt_Parse(char const *s) {
unsigned long maxRecursionDepth = strtoul(s, &endptr, 10);
if (*endptr != '\0') {
error("Invalid argument to option 'r' (\"%s\")", s);
error("Invalid argument for option 'r' (\"%s\")", s);
} else if (errno == ERANGE) {
error("Argument to 'r' is out of range (\"%s\")", s);
error("Argument for option 'r' is out of range (\"%s\")", s);
} else {
opt_R(maxRecursionDepth);
}

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);
@@ -159,15 +162,10 @@ static void writeRpn(std::vector<uint8_t> &rpnexpr, std::vector<uint8_t> const &
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++] = RPN_SYM;
rpnexpr[rpnptr++] = value & 0xFF;
rpnexpr[rpnptr++] = value >> 8;
rpnexpr[rpnptr++] = value >> 16;
@@ -273,7 +271,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 {
@@ -303,7 +303,7 @@ void out_WriteObject() {
if (!file) {
// LCOV_EXCL_START
fatal(
"Failed to open object file '%s': %s", options.objectFileName.c_str(), strerror(errno)
"Failed to open object file \"%s\": %s", options.objectFileName.c_str(), strerror(errno)
);
// LCOV_EXCL_STOP
}
@@ -485,7 +485,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;
@@ -47,7 +43,6 @@
#include "extern/utf8decoder.hpp"
#include "helpers.hpp"
#include "asm/actions.hpp"
#include "asm/charmap.hpp"
#include "asm/fixpoint.hpp"
#include "asm/fstack.hpp"
@@ -103,6 +98,7 @@
%token LBRACK "[" RBRACK "]"
%token LBRACKS "[[" RBRACKS "]]"
%token LPAREN "(" RPAREN ")"
%token QUESTIONMARK "?"
// Arithmetic operators
%token OP_ADD "+" OP_SUB "-"
@@ -327,6 +323,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 +400,7 @@
%type <SectionType> sect_type
%type <StrFmtArgList> strfmt_args
%type <StrFmtArgList> strfmt_va_args
%type <bool> maybe_quiet
%%
@@ -428,12 +426,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 +465,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 +485,14 @@ else:
plain_directive:
label
| label cpu_commands
| label data
| label macro
| label directive
;
endc:
POP_ENDC {
lexer_DecIFDepth();
act_Endc();
}
;
@@ -576,7 +547,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 +573,6 @@ directive:
| println
| export
| export_def
| db
| dw
| dl
| ds
| section
| rsreset
| rsset
@@ -684,29 +657,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 +750,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 +789,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 +809,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 +853,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 +905,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 +1020,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;
}
}
@@ -1363,17 +1262,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 +1282,7 @@ 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));
}
| OP_LOGICNOT relocexpr %prec NEG {
$$.makeUnaryOp(RPN_LOGNOT, std::move($2));
@@ -1568,10 +1459,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 +1491,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 +1555,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;
@@ -1745,23 +1602,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 +1614,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 +1741,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 +1801,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);
@@ -2129,26 +2024,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.makeCheckHRAM();
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.makeCheckHRAM();
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 +2061,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 +2088,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 +2101,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 +2124,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 +2139,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 +2157,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 +2173,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],
@@ -2544,7 +2427,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,16 @@
#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 "helpers.hpp" // assume, clz, ctz
#include "helpers.hpp" // assume
#include "linkdefs.hpp"
#include "opmath.hpp"
#include "asm/output.hpp"
@@ -77,15 +81,16 @@ void Expression::makeSymbol(std::string const &symName) {
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!
@@ -115,7 +120,7 @@ void Expression::makeBankSymbol(std::string const &symName) {
}
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);
@@ -126,8 +131,8 @@ void Expression::makeBankSymbol(std::string const &symName) {
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";
? "`"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!
@@ -253,7 +258,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.
@@ -301,12 +306,9 @@ void Expression::makeUnaryOp(RPNCommand op, Expression &&src) {
// 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 +317,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:
@@ -397,37 +399,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 +432,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 +448,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 +458,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
@@ -535,20 +527,16 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
}
}
bool Expression::makeCheckHRAM() {
void Expression::makeCheckHRAM() {
isSymbol = false;
if (!isKnown()) {
*reserveSpace(1) = 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() {
@@ -556,7 +544,7 @@ void Expression::makeCheckRST() {
*reserveSpace(1) = 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);
}
}
@@ -569,7 +557,7 @@ void Expression::makeCheckBitIndex(uint8_t mask) {
*ptr = 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 +579,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,7 +589,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;
}

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,26 @@ static unsigned int
}
} else if (alignment != 0) {
int32_t curOfs = (alignOffset - sect.size) % (1U << alignment);
int32_t curOfs = (alignOffset - sect.size) % alignSize;
if (curOfs < 0) {
curOfs += 1U << alignment;
curOfs += alignSize;
}
// Make sure any fixed address given is compatible
if (sect.org != UINT32_MAX) {
if ((sect.org - curOfs) & mask(alignment)) {
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 +263,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 +290,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 +323,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 +339,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 +347,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 +364,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 +381,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 +401,41 @@ 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) {
if (alignOffset >= alignSize) {
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;
}
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 +466,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 +475,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 +512,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 +544,7 @@ void sect_SetLoadSection(
return;
}
if (sect_HasData(type)) {
if (sectTypeHasData(type)) {
error("`LOAD` blocks cannot create a ROM section");
return;
}
@@ -609,8 +632,12 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
return;
}
assume(alignment <= 16); // Should be ensured by the caller
uint32_t alignSize = 1u << alignment;
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;
if (sect->org != UINT32_MAX) {
if (uint32_t actualOffset = (sect->org + curOffset) % alignSize; actualOffset != offset) {
@@ -624,26 +651,20 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
actualOffset
);
}
} else {
if (uint32_t actualOffset = (sect->alignOfs + curOffset) % alignSize,
sectAlignSize = 1 << sect->align;
} else if (uint32_t actualOffset = (sect->alignOfs + curOffset) % alignSize;
sect->align != 0 && actualOffset % sectAlignSize != offset % sectAlignSize) {
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) {
@@ -652,7 +673,6 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
sect->alignOfs = (offset - curOffset) % alignSize;
}
}
}
static void growSection(uint32_t growth) {
if (growth > 0 && curOffset > UINT32_MAX - growth) {
@@ -697,11 +717,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 +740,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 +748,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 +758,7 @@ void sect_EndUnion() {
void sect_CheckUnionClosed() {
if (!currentUnionStack.empty()) {
error("Unterminated UNION construct");
error("Unterminated `UNION` construct");
}
}
@@ -797,13 +817,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 +912,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,19 +939,25 @@ 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 {
if (errno != ESPIPE) {
error("Error determining size of INCBIN file '%s': %s", name.c_str(), strerror(errno));
// LCOV_EXCL_START
error(
"Error determining size of `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno)
);
// LCOV_EXCL_STOP
}
// 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;
}
}
@@ -942,7 +968,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 +994,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,
@@ -983,12 +1011,18 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l
fseek(file, startPos, SEEK_SET);
} else {
if (errno != ESPIPE) {
error("Error determining size of INCBIN file '%s': %s", name.c_str(), strerror(errno));
// LCOV_EXCL_START
error(
"Error determining size of `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno)
);
// LCOV_EXCL_STOP
}
// 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;
}
}
@@ -998,10 +1032,12 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l
if (int byte = fgetc(file); byte != EOF) {
writeByte(byte);
} else 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
} 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
);
@@ -1056,11 +1092,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 +1113,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;
@@ -39,8 +48,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 +62,14 @@ 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> 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 +77,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 +121,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 +150,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 +213,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 +245,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 +260,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;
@@ -285,19 +309,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 +360,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 +398,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 +429,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 +464,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 +518,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 +594,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 +611,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 +620,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 +638,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 +661,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);
@@ -702,8 +723,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 },
@@ -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;
}

29
src/backtrace.cpp Normal file
View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
#include "backtrace.hpp"
#include <stdlib.h> // strtoul
#include "platform.hpp" // strcasecmp
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 {
char *endptr;
tracing.depth = strtoul(arg, &endptr, 0);
return arg[0] != '\0' && *endptr == '\0';
}
}

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);
@@ -49,12 +64,12 @@ std::pair<WarningState, std::optional<uint32_t>> getInitialWarningState(std::str
}
// Is the rest of the string a decimal number?
// We want to avoid `strtoul`'s whitespace and sign, so we parse manually
// We want to avoid `strtoul`'s whitespace and sign handling, so we parse manually
char const *ptr = flag.c_str() + equals + 1;
uint32_t param = 0;
bool overflowed = false;
for (; *ptr >= '0' && *ptr <= '9'; ++ptr) {
for (; isDigit(*ptr); ++ptr) {
if (overflowed) {
continue;
}

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

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

@@ -0,0 +1,522 @@
// 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), options.titleLen, "title"
);
}
if (options.gameID) {
overwriteBytes(
rom0,
0x13F,
reinterpret_cast<uint8_t const *>(options.gameID),
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),
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
uint8_t bank[BANK_SIZE]; // Temp buffer used to store a whole bank's worth of data
// Handle ROMX
if (input == output) {
if (fileSize >= 0x10000 * BANK_SIZE) {
error("\"%s\" has more than 65536 banks", name);
return;
}
// This should be guaranteed from the size cap...
static_assert(0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS");
// 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
static_assert(
0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS"
);
if (nbBanks == 0x10000) {
error("\"%s\" has more than 65536 banks", name);
return;
}
++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 (;;) {
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
}
}
memset(bank, options.padValue, sizeof(bank));
size_t len = (nbBanks - 1) * BANK_SIZE - 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) {
error("Failed to open \"%s\" for writing: %s", outputName, strerror(errno));
return true;
}
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);
}

View File

@@ -1,7 +1,6 @@
// SPDX-License-Identifier: MIT
#include <sys/stat.h>
#include <sys/types.h>
#include "fix/main.hpp"
#include <errno.h>
#include <limits.h>
@@ -10,23 +9,27 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include "diagnostics.hpp"
#include "extern/getopt.hpp"
#include "helpers.hpp"
#include "platform.hpp"
#include "style.hpp"
#include "usage.hpp"
#include "version.hpp"
#include "fix/fix.hpp"
#include "fix/mbc.hpp"
#include "fix/warning.hpp"
static constexpr off_t BANK_SIZE = 0x4000;
Options options;
// Short options
static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:VvW:w";
// Variables for the long-only options
static int longOpt; // `--color`
// Equivalent long options
// Please keep in the same order as short opts.
// Also, make sure long opts don't create ambiguity:
@@ -55,553 +58,39 @@ static option const longopts[] = {
{"version", no_argument, nullptr, 'V'},
{"validate", no_argument, nullptr, 'v'},
{"warning", required_argument, nullptr, 'W'},
{nullptr, no_argument, nullptr, 0 }
{"color", required_argument, &longOpt, 'c'},
{nullptr, no_argument, nullptr, 0 },
};
// clang-format off: long string literal
static Usage usage(
"Usage: rgbfix [-hjOsVvw] [-C | -c] [-f <fix_spec>] [-i <game_id>] [-k <licensee>]\n"
" [-L <logo_file>] [-l <licensee_byte>] [-m <mbc_type>]\n"
" [-n <rom_version>] [-p <pad_value>] [-r <ram_size>] [-t <title_str>]\n"
" [-W warning] <file> ...\n"
"Useful options:\n"
" -m, --mbc-type <value> set the MBC type byte to this value; refer\n"
" to the man page for a list of values\n"
" -p, --pad-value <value> pad to the next valid size using this value\n"
" -r, --ram-size <code> set the cart RAM size byte to this value\n"
" -o, --output <path> set the output file\n"
" -V, --version print RGBFIX version and exit\n"
" -v, --validate fix the header logo and both checksums (-f lhg)\n"
"\n"
"For help, use `man rgbfix' or go to https://rgbds.gbdev.io/docs/\n"
);
// clang-format off: nested initializers
static Usage usage = {
.name = "rgbfix",
.flags = {
"[-hjOsVvw]", "[-C | -c]", "[-f <fix_spec>]", "[-i <game_id>]", "[-k <licensee>]",
"[-L <logo_file>]", "[-l <licensee_byte>]", "[-m <mbc_type>]", "[-n <rom_version>]",
"[-p <pad_value>]", "[-r <ram_size>]", "[-t <title_str>]", "[-W warning]", "<file> ...",
},
.options = {
{
{"-m", "--mbc-type <value>"},
{
"set the MBC type byte to this value; \"-m help\"",
"or \"-m list\" prints the accepted values",
},
},
{{"-p", "--pad-value <value>"}, {"pad to the next valid size using this value"}},
{{"-r", "--ram-size <code>"}, {"set the cart RAM size byte to this value"}},
{{"-o", "--output <path>"}, {"set the output file"}},
{{"-V", "--version"}, {"print RGBFIX version and exit"}},
{{"-v", "--validate"}, {"fix the header logo and both checksums (\"-f lhg\")"}},
{{"-W", "--warning <warning>"}, {"enable or disable warnings"}},
},
};
// clang-format on
static uint8_t tpp1Rev[2];
static uint8_t const nintendoLogo[] = {
0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D,
0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99,
0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E,
};
static uint8_t fixSpec = 0;
// 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
static enum { DMG, BOTH, CGB } model = DMG; // If DMG, byte is left alone
static char const *gameID = nullptr;
static uint8_t gameIDLen;
static bool japanese = true;
static char const *logoFilename = nullptr;
static uint8_t logo[sizeof(nintendoLogo)] = {};
static char const *newLicensee = nullptr;
static uint8_t newLicenseeLen;
static uint16_t oldLicensee = UNSPECIFIED;
static MbcType cartridgeType = MBC_NONE;
static uint16_t romVersion = UNSPECIFIED;
static uint16_t padValue = UNSPECIFIED;
static uint16_t ramSize = UNSPECIFIED;
static bool sgb = false; // If false, SGB flags are left alone
static char const *title = nullptr;
static uint8_t titleLen;
static uint8_t maxTitleLen() {
return gameID ? 11 : model != DMG ? 15 : 16;
}
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 = (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 (fixSpec & (FIX_LOGO | TRASH_LOGO)) {
overwriteBytes(rom0, 0x0104, logo, sizeof(logo), logoFilename ? "logo" : "Nintendo logo");
}
if (title) {
overwriteBytes(rom0, 0x134, reinterpret_cast<uint8_t const *>(title), titleLen, "title");
}
if (gameID) {
overwriteBytes(
rom0, 0x13F, reinterpret_cast<uint8_t const *>(gameID), gameIDLen, "manufacturer code"
);
}
if (model != DMG) {
overwriteByte(rom0, 0x143, model == BOTH ? 0x80 : 0xC0, "CGB flag");
}
if (newLicensee) {
overwriteBytes(
rom0,
0x144,
reinterpret_cast<uint8_t const *>(newLicensee),
newLicenseeLen,
"new licensee code"
);
}
if (sgb) {
overwriteByte(rom0, 0x146, 0x03, "SGB flag");
}
// If a valid MBC was specified...
if (cartridgeType < MBC_NONE) {
uint8_t byte = cartridgeType;
if ((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 ((cartridgeType & 0xFF00) == TPP1) {
uint8_t const tpp1Code[2] = {0xC1, 0x65};
overwriteBytes(rom0, 0x149, tpp1Code, sizeof(tpp1Code), "TPP1 identification code");
overwriteBytes(rom0, 0x150, tpp1Rev, sizeof(tpp1Rev), "TPP1 revision number");
if (ramSize != UNSPECIFIED) {
overwriteByte(rom0, 0x152, ramSize, "RAM size");
}
overwriteByte(rom0, 0x153, cartridgeType & 0xFF, "TPP1 feature flags");
} else {
// Regular mappers
if (ramSize != UNSPECIFIED) {
overwriteByte(rom0, 0x149, ramSize, "RAM size");
}
if (!japanese) {
overwriteByte(rom0, 0x14A, 0x01, "destination code");
}
}
if (oldLicensee != UNSPECIFIED) {
overwriteByte(rom0, 0x14B, oldLicensee, "old licensee code");
} else if (sgb && rom0[0x14B] != 0x33) {
warning(
WARNING_SGB,
"SGB compatibility enabled, but old licensee was 0x%02x, not 0x33",
rom0[0x14B]
);
}
if (romVersion != UNSPECIFIED) {
overwriteByte(rom0, 0x14C, 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
uint8_t bank[BANK_SIZE]; // Temp buffer used to store a whole bank's worth of data
// Handle ROMX
if (input == output) {
if (fileSize >= 0x10000 * BANK_SIZE) {
error("\"%s\" has more than 65536 banks", name);
return;
}
// This should be guaranteed from the size cap...
static_assert(0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS");
// 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 gonna read another bank, check that it won't be too much
static_assert(
0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS"
);
if (nbBanks == 0x10000) {
error("\"%s\" has more than 65536 banks", name);
return;
}
++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 (padValue != UNSPECIFIED) {
// We want at least 2 banks
if (nbBanks == 1) {
if (rom0Len != sizeof(rom0)) {
memset(&rom0[rom0Len], 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 += padValue * ((nbBanks - 1) * BANK_SIZE - totalRomxLen);
}
// Handle the header checksum after the ROM size has been written
if (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, fixSpec & TRASH_HEADER_SUM ? ~sum : sum, "header checksum");
}
if (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 (;;) {
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 (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 (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 (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
}
}
memset(bank, padValue, sizeof(bank));
size_t len = (nbBanks - 1) * BANK_SIZE - 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;
}
}
}
static bool processFilename(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) {
error("Failed to open \"%s\" for writing: %s", outputName, strerror(errno));
return true;
}
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);
}
static void parseByte(uint16_t &output, char name) {
if (musl_optarg[0] == 0) {
fatal("Argument to option '%c' may not be empty", name);
fatal("Argument to option '-%c' may not be empty", name);
}
char *endptr;
@@ -613,33 +102,39 @@ static void parseByte(uint16_t &output, char name) {
}
if (*endptr) {
fatal("Expected number as argument to option '%c', got %s", name, musl_optarg);
fatal("Expected number as argument to option '-%c', got \"%s\"", name, musl_optarg);
} else if (value > 0xFF) {
fatal("Argument to option '%c' is larger than 255: %lu", name, value);
fatal("Argument to option '-%c' is larger than 255: %lu", name, value);
}
output = value;
}
static uint8_t const nintendoLogo[] = {
0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D,
0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99,
0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E,
};
static void initLogo() {
if (logoFilename) {
if (options.logoFilename) {
FILE *logoFile;
if (strcmp(logoFilename, "-")) {
logoFile = fopen(logoFilename, "rb");
if (strcmp(options.logoFilename, "-")) {
logoFile = fopen(options.logoFilename, "rb");
} else {
logoFilename = "<stdin>";
options.logoFilename = "<stdin>";
(void)setmode(STDIN_FILENO, O_BINARY);
logoFile = stdin;
}
if (!logoFile) {
fatal("Failed to open \"%s\" for reading: %s", logoFilename, strerror(errno));
fatal("Failed to open \"%s\" for reading: %s", options.logoFilename, strerror(errno));
}
Defer closeLogo{[&] { fclose(logoFile); }};
uint8_t logoBpp[sizeof(logo)];
uint8_t logoBpp[sizeof(options.logo)];
if (size_t nbRead = fread(logoBpp, 1, sizeof(logoBpp), logoFile);
nbRead != sizeof(logo) || fgetc(logoFile) != EOF || ferror(logoFile)) {
fatal("\"%s\" is not %zu bytes", logoFilename, sizeof(logo));
nbRead != sizeof(options.logo) || fgetc(logoFile) != EOF || ferror(logoFile)) {
fatal("\"%s\" is not %zu bytes", options.logoFilename, sizeof(options.logo));
}
auto highs = [&logoBpp](size_t i) {
return (logoBpp[i * 2] & 0xF0) | ((logoBpp[i * 2 + 1] & 0xF0) >> 4);
@@ -647,53 +142,55 @@ static void initLogo() {
auto lows = [&logoBpp](size_t i) {
return ((logoBpp[i * 2] & 0x0F) << 4) | (logoBpp[i * 2 + 1] & 0x0F);
};
constexpr size_t mid = sizeof(logo) / 2;
constexpr size_t mid = sizeof(options.logo) / 2;
for (size_t i = 0; i < mid; i += 4) {
logo[i + 0] = highs(i + 0);
logo[i + 1] = highs(i + 1);
logo[i + 2] = lows(i + 0);
logo[i + 3] = lows(i + 1);
logo[mid + i + 0] = highs(i + 2);
logo[mid + i + 1] = highs(i + 3);
logo[mid + i + 2] = lows(i + 2);
logo[mid + i + 3] = lows(i + 3);
options.logo[i + 0] = highs(i + 0);
options.logo[i + 1] = highs(i + 1);
options.logo[i + 2] = lows(i + 0);
options.logo[i + 3] = lows(i + 1);
options.logo[mid + i + 0] = highs(i + 2);
options.logo[mid + i + 1] = highs(i + 3);
options.logo[mid + i + 2] = lows(i + 2);
options.logo[mid + i + 3] = lows(i + 3);
}
} else {
memcpy(logo, nintendoLogo, sizeof(nintendoLogo));
static_assert(sizeof(options.logo) == sizeof(nintendoLogo));
memcpy(options.logo, nintendoLogo, sizeof(nintendoLogo));
}
if (fixSpec & TRASH_LOGO) {
for (uint16_t i = 0; i < sizeof(logo); ++i) {
logo[i] = 0xFF ^ logo[i];
if (options.fixSpec & TRASH_LOGO) {
for (uint16_t i = 0; i < sizeof(options.logo); ++i) {
options.logo[i] = 0xFF ^ options.logo[i];
}
}
}
int main(int argc, char *argv[]) {
char const *outputFilename = nullptr;
// Parse CLI options
for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) {
switch (ch) {
size_t len;
case 'C':
case 'c':
model = ch == 'c' ? BOTH : CGB;
if (titleLen > 15) {
titleLen = 15;
warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", title);
options.model = ch == 'c' ? BOTH : CGB;
if (options.titleLen > 15) {
options.titleLen = 15;
assume(options.title != nullptr);
warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", options.title);
}
break;
case 'f':
fixSpec = 0;
options.fixSpec = 0;
while (*musl_optarg) {
switch (*musl_optarg) {
#define OVERRIDE_SPEC(cur, bad, curFlag, badFlag) \
case STR(cur)[0]: \
if (fixSpec & badFlag) { \
if (options.fixSpec & badFlag) { \
warnx("'" STR(cur) "' overriding '" STR(bad) "' in fix spec"); \
} \
fixSpec = (fixSpec & ~badFlag) | curFlag; \
options.fixSpec = (options.fixSpec & ~badFlag) | curFlag; \
break
#define overrideSpecs(fix, fixFlag, trash, trashFlag) \
OVERRIDE_SPEC(fix, trash, fixFlag, trashFlag); \
@@ -711,50 +208,58 @@ int main(int argc, char *argv[]) {
}
break;
// LCOV_EXCL_START
case 'h':
usage.printAndExit(0); // LCOV_EXCL_LINE
usage.printAndExit(0);
// LCOV_EXCL_STOP
case 'i':
gameID = musl_optarg;
len = strlen(gameID);
case 'i': {
options.gameID = musl_optarg;
size_t len = strlen(options.gameID);
if (len > 4) {
len = 4;
warning(WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", gameID);
warning(WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", options.gameID);
}
gameIDLen = len;
if (titleLen > 11) {
titleLen = 11;
warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", title);
options.gameIDLen = len;
if (options.titleLen > 11) {
options.titleLen = 11;
assume(options.title != nullptr);
warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", options.title);
}
break;
}
case 'j':
japanese = false;
options.japanese = false;
break;
case 'k':
newLicensee = musl_optarg;
len = strlen(newLicensee);
case 'k': {
options.newLicensee = musl_optarg;
size_t len = strlen(options.newLicensee);
if (len > 2) {
len = 2;
warning(
WARNING_TRUNCATION, "Truncating new licensee \"%s\" to 2 chars", newLicensee
WARNING_TRUNCATION,
"Truncating new licensee \"%s\" to 2 chars",
options.newLicensee
);
}
newLicenseeLen = len;
options.newLicenseeLen = len;
break;
}
case 'L':
logoFilename = musl_optarg;
options.logoFilename = musl_optarg;
break;
case 'l':
parseByte(oldLicensee, 'l');
parseByte(options.oldLicensee, 'l');
break;
case 'm':
cartridgeType = mbc_ParseName(musl_optarg, tpp1Rev[0], tpp1Rev[1]);
if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) {
options.cartridgeType =
mbc_ParseName(musl_optarg, options.tpp1Rev[0], options.tpp1Rev[1]);
if (options.cartridgeType == ROM_RAM || options.cartridgeType == ROM_RAM_BATTERY) {
warning(
WARNING_MBC, "MBC \"%s\" is under-specified and poorly supported", musl_optarg
);
@@ -762,10 +267,11 @@ int main(int argc, char *argv[]) {
break;
case 'n':
parseByte(romVersion, 'n');
parseByte(options.romVersion, 'n');
break;
case 'O':
warning(WARNING_OBSOLETE, "'-O' is deprecated; use '-Wno-overwrite' instead");
warnings.processWarningFlag("no-overwrite");
break;
@@ -774,39 +280,41 @@ int main(int argc, char *argv[]) {
break;
case 'p':
parseByte(padValue, 'p');
parseByte(options.padValue, 'p');
break;
case 'r':
parseByte(ramSize, 'r');
parseByte(options.ramSize, 'r');
break;
case 's':
sgb = true;
options.sgb = true;
break;
case 't': {
title = musl_optarg;
len = strlen(title);
uint8_t maxLen = maxTitleLen();
options.title = musl_optarg;
size_t len = strlen(options.title);
uint8_t maxLen = options.gameID ? 11 : options.model != DMG ? 15 : 16;
if (len > maxLen) {
len = maxLen;
warning(WARNING_TRUNCATION, "Truncating title \"%s\" to %u chars", title, maxLen);
warning(
WARNING_TRUNCATION, "Truncating title \"%s\" to %u chars", options.title, maxLen
);
}
titleLen = len;
options.titleLen = len;
break;
}
case 'V':
// LCOV_EXCL_START
case 'V':
printf("rgbfix %s\n", get_package_version_string());
exit(0);
// LCOV_EXCL_STOP
case 'v':
fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM;
options.fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM;
break;
// LCOV_EXCL_STOP
case 'W':
warnings.processWarningFlag(musl_optarg);
@@ -816,56 +324,62 @@ int main(int argc, char *argv[]) {
warnings.state.warningsEnabled = false;
break;
case 0: // Long-only options
if (longOpt == 'c' && !style_Parse(musl_optarg)) {
fatal("Invalid argument for option '--color'");
}
break;
default:
usage.printAndExit(1); // LCOV_EXCL_LINE
}
}
if ((cartridgeType & 0xFF00) == TPP1 && !japanese) {
if ((options.cartridgeType & 0xFF00) == TPP1 && !options.japanese) {
warning(
WARNING_MBC, "TPP1 overwrites region flag for its identification code, ignoring `-j`"
WARNING_MBC, "TPP1 overwrites region flag for its identification code, ignoring '-j'"
);
}
// Check that RAM size is correct for "standard" mappers
if (ramSize != UNSPECIFIED && (cartridgeType & 0xFF00) == 0) {
if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) {
if (ramSize != 1) {
if (options.ramSize != UNSPECIFIED && (options.cartridgeType & 0xFF00) == 0) {
if (options.cartridgeType == ROM_RAM || options.cartridgeType == ROM_RAM_BATTERY) {
if (options.ramSize != 1) {
warning(
WARNING_MBC,
"MBC \"%s\" should have 2 KiB of RAM (-r 1)",
mbc_Name(cartridgeType)
"MBC \"%s\" should have 2 KiB of RAM (\"-r 1\")",
mbc_Name(options.cartridgeType)
);
}
} else if (mbc_HasRAM(cartridgeType)) {
if (!ramSize) {
} else if (mbc_HasRAM(options.cartridgeType)) {
if (!options.ramSize) {
warning(
WARNING_MBC,
"MBC \"%s\" has RAM, but RAM size was set to 0",
mbc_Name(cartridgeType)
mbc_Name(options.cartridgeType)
);
} else if (ramSize == 1) {
} else if (options.ramSize == 1) {
warning(
WARNING_MBC,
"RAM size 1 (2 KiB) was specified for MBC \"%s\"",
mbc_Name(cartridgeType)
mbc_Name(options.cartridgeType)
);
}
} else if (ramSize) {
} else if (options.ramSize) {
warning(
WARNING_MBC,
"MBC \"%s\" has no RAM, but RAM size was set to %u",
mbc_Name(cartridgeType),
ramSize
mbc_Name(options.cartridgeType),
options.ramSize
);
}
}
if (sgb && oldLicensee != UNSPECIFIED && oldLicensee != 0x33) {
if (options.sgb && options.oldLicensee != UNSPECIFIED && options.oldLicensee != 0x33) {
warning(
WARNING_SGB,
"SGB compatibility enabled, but old licensee is 0x%02x, not 0x33",
oldLicensee
options.oldLicensee
);
}
@@ -873,16 +387,16 @@ int main(int argc, char *argv[]) {
argv += musl_optind;
if (!*argv) {
usage.printAndExit("Please specify an input file (pass `-` to read from standard input)");
usage.printAndExit("No input file specified (pass \"-\" to read from standard input)");
}
if (outputFilename && argc != musl_optind + 1) {
usage.printAndExit("If `-o` is set then only a single input file may be specified");
usage.printAndExit("If '-o' is set then only a single input file may be specified");
}
bool failed = warnings.nbErrors > 0;
do {
failed |= processFilename(*argv, outputFilename);
failed |= fix_ProcessFile(*argv, outputFilename);
} while (*++argv);
return failed;

View File

@@ -1,11 +1,16 @@
// SPDX-License-Identifier: MIT
#include "fix/mbc.hpp"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unordered_map>
#include <utility>
#include "helpers.hpp" // unreachable_
#include "platform.hpp" // strcasecmp
#include "util.hpp" // isBlankSpace, isDigit
#include "fix/warning.hpp"
@@ -91,14 +96,14 @@ bool mbc_HasRAM(MbcType type) {
return search != mbcData.end() && search->second.second;
}
static void skipWhitespace(char const *&ptr) {
while (*ptr == ' ' || *ptr == '\t') {
static void skipBlankSpace(char const *&ptr) {
while (isBlankSpace(*ptr)) {
++ptr;
}
}
static void skipMBCSpace(char const *&ptr) {
while (*ptr == ' ' || *ptr == '\t' || *ptr == '_') {
while (isBlankSpace(*ptr) || *ptr == '_') {
++ptr;
}
}
@@ -140,7 +145,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
exit(0);
}
if ((name[0] >= '0' && name[0] <= '9') || name[0] == '$') {
if (isDigit(name[0]) || name[0] == '$') {
int base = 0;
if (name[0] == '$') {
@@ -155,7 +160,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
fatalUnknownMBC(fullName);
}
if (mbc > 0xFF) {
fatal("Specified MBC ID out of range 0-255: %s", fullName);
fatal("Specified MBC ID out of range 0-255: \"%s\"", fullName);
}
return static_cast<MbcType>(mbc);
}
@@ -164,7 +169,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
uint16_t mbc;
char const *ptr = name;
skipWhitespace(ptr); // Trim off leading whitespace
skipBlankSpace(ptr); // Trim off leading blank space
#define tryReadSlice(expected) \
do { \
@@ -316,7 +321,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
// clang-format on
for (;;) {
skipWhitespace(ptr); // Trim off trailing whitespace
skipBlankSpace(ptr); // Trim off trailing blank space
// If done, start processing "features"
if (!*ptr) {
@@ -413,7 +418,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;
@@ -499,9 +504,9 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
break;
}
skipWhitespace(ptr); // Trim off trailing whitespace
skipBlankSpace(ptr); // Trim off trailing blank space
// If there is still something past the whitespace, error out
// If there is still something left, error out
if (*ptr) {
fatalUnknownMBC(fullName);
}

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"

View File

@@ -4,10 +4,11 @@
#include <algorithm>
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <ios>
#include <limits>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -17,13 +18,18 @@
#include "diagnostics.hpp"
#include "extern/getopt.hpp"
#include "file.hpp"
#include "helpers.hpp"
#include "platform.hpp"
#include "style.hpp"
#include "usage.hpp"
#include "util.hpp"
#include "verbosity.hpp"
#include "version.hpp"
#include "gfx/pal_spec.hpp"
#include "gfx/process.hpp"
#include "gfx/reverse.hpp"
#include "gfx/rgba.hpp"
#include "gfx/warning.hpp"
using namespace std::literals::string_view_literals;
@@ -40,21 +46,12 @@ static struct LocalOptions {
bool reverse;
} localOptions;
void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
// LCOV_EXCL_START
if (verbosity >= level) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
// LCOV_EXCL_STOP
}
// Short options
static char const *optstring = "-Aa:B:b:Cc:d:hi:L:l:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvW:wXx:YZ";
// Variables for the long-only options
static int longOpt; // `--color`
// Equivalent long options
// Please keep in the same order as short opts.
// Also, make sure long opts don't create ambiguity:
@@ -96,25 +93,29 @@ static option const longopts[] = {
{"trim-end", required_argument, nullptr, 'x'},
{"mirror-y", no_argument, nullptr, 'Y'},
{"columns", no_argument, nullptr, 'Z'},
{nullptr, no_argument, nullptr, 0 }
{"color", required_argument, &longOpt, 'c'},
{nullptr, no_argument, nullptr, 0 },
};
// clang-format off: long string literal
static Usage usage(
"Usage: rgbgfx [-r stride] [-ChmOuVXYZ] [-v [-v ...]] [-a <attr_map> | -A]\n"
" [-b <base_ids>] [-c <colors>] [-d <depth>] [-i <tileset_file>]\n"
" [-L <slice>] [-l <base_pal>] [-N <nb_tiles>] [-n <nb_pals>]\n"
" [-o <out_file>] [-p <pal_file> | -P] [-q <pal_map> | -Q]\n"
" [-s <nb_colors>] [-t <tile_map> | -T] [-x <nb_tiles>] <file>\n"
"Useful options:\n"
" -m, --mirror-tiles optimize out mirrored tiles\n"
" -o, --output <path> output the tile data to this path\n"
" -t, --tilemap <path> output the tile map to this path\n"
" -u, --unique-tiles optimize out identical tiles\n"
" -V, --version print RGBGFX version and exit\n"
"\n"
"For help, use `man rgbgfx' or go to https://rgbds.gbdev.io/docs/\n"
);
// clang-format off: nested initializers
static Usage usage = {
.name = "rgbgfx",
.flags = {
"[-r stride]", "[-ChmOuVXYZ]", "[-v [-v ...]]", "[-a <attr_map> | -A]", "[-b <base_ids>]",
"[-c <colors>]", "[-d <depth>]", "[-i <tileset_file>]", "[-L <slice>]", "[-l <base_pal>]",
"[-N <nb_tiles>]", "[-n <nb_pals>]", "[-o <out_file>]", "[-p <pal_file> | -P]",
"[-q <pal_map> | -Q]", "[-s <nb_colors>]", "[-t <tile_map> | -T]", "[-x <nb_tiles>]",
"<file>",
},
.options = {
{{"-m", "--mirror-tiles"}, {"optimize out mirrored tiles"}},
{{"-o", "--output <path>"}, {"output the tile data to this path"}},
{{"-t", "--tilemap <path>"}, {"output the tile map to this path"}},
{{"-u", "--unique-tiles"}, {"optimize out identical tiles"}},
{{"-V", "--version"}, {"print RGBGFX version and exit"}},
{{"-W", "--warning <warning>"}, {"enable or disable warnings"}},
},
};
// clang-format on
// Parses a number at the beginning of a string, moving the pointer to skip the parsed characters.
@@ -188,7 +189,7 @@ static uint16_t parseNumber(char *&string, char const *errPrefix, uint16_t errVa
return number;
}
static void skipWhitespace(char *&arg) {
static void skipBlankSpace(char *&arg) {
arg += strspn(arg, " \t");
}
@@ -210,71 +211,47 @@ static void registerInput(char const *arg) {
static std::vector<size_t> readAtFile(std::string const &path, std::vector<char> &argPool) {
File file;
if (!file.open(path, std::ios_base::in)) {
fatal("Error reading @%s: %s", file.c_str(path), strerror(errno));
fatal("Error reading at-file \"%s\": %s", file.c_str(path), strerror(errno));
}
// We only filter out `EOF`, but calling `isblank()` on anything else is UB!
static_assert(
std::streambuf::traits_type::eof() == EOF,
"isblank(std::streambuf::traits_type::eof()) is UB!"
);
std::vector<size_t> argvOfs;
for (std::vector<size_t> argvOfs;;) {
int c = file->sbumpc();
for (;;) {
int c;
// First, discard any leading whitespace
do {
// First, discard any leading blank space
while (isBlankSpace(c)) {
c = file->sbumpc();
if (c == EOF) {
return argvOfs;
}
} while (isblank(c));
switch (c) {
case '#': // If it's a comment, discard everything until EOL
while ((c = file->sbumpc()) != '\n') {
// 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;
}
}
continue; // Start processing the next line
// If it's an empty line, ignore it
case '\r': // Assuming CRLF here
file->sbumpc(); // Discard the upcoming '\n'
[[fallthrough]];
case '\n':
} 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`.
argvOfs.push_back(argPool.size());
// Reading and appending characters one at a time may be inefficient, but I'm counting
// on `vector` and `sbumpc` to do the right thing here.
argPool.push_back(c); // Push the character we've already read
for (;;) {
c = file->sbumpc();
if (c == EOF || c == '\n' || isblank(c)) {
break;
} else if (c == '\r') {
file->sbumpc(); // Discard the '\n'
break;
}
for (; c != EOF && !isWhitespace(c); c = file->sbumpc()) {
argPool.push_back(c);
}
argPool.push_back('\0');
// Discard whitespace until the next argument (candidate)
while (isblank(c)) {
// Discard blank space until the next argument (candidate)
while (isBlankSpace(c)) {
c = file->sbumpc();
}
if (c == '\r') {
c = file->sbumpc(); // Skip the '\n'
}
} while (c != '\n' && c != EOF); // End if we reached EOL
} while (c != EOF && !isNewline(c)); // End if we reached EOL
}
}
@@ -285,23 +262,26 @@ static std::vector<size_t> readAtFile(std::string const &path, std::vector<char>
static char *parseArgv(int argc, char *argv[]) {
for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) {
char *arg = musl_optarg; // Make a copy for scanning
uint16_t number;
switch (ch) {
case 'A':
localOptions.autoAttrmap = true;
break;
case 'a':
localOptions.autoAttrmap = false;
if (!options.attrmap.empty()) {
warnx("Overriding attrmap file %s", options.attrmap.c_str());
warnx("Overriding attrmap file \"%s\"", options.attrmap.c_str());
}
options.attrmap = musl_optarg;
break;
case 'B':
parseBackgroundPalSpec(musl_optarg);
break;
case 'b':
number = parseNumber(arg, "Bank 0 base tile ID", 0);
case 'b': {
uint16_t number = parseNumber(arg, "Bank 0 base tile ID", 0);
if (number >= 256) {
error("Bank 0 base tile ID must be below 256");
} else {
@@ -311,7 +291,7 @@ static char *parseArgv(int argc, char *argv[]) {
options.baseTileIDs[1] = 0;
break;
}
skipWhitespace(arg);
skipBlankSpace(arg);
if (*arg != ',') {
error(
"Base tile IDs must be one or two comma-separated numbers, not \"%s\"",
@@ -320,7 +300,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
}
++arg; // Skip comma
skipWhitespace(arg);
skipBlankSpace(arg);
number = parseNumber(arg, "Bank 1 base tile ID", 0);
if (number >= 256) {
error("Bank 1 base tile ID must be below 256");
@@ -335,9 +315,12 @@ static char *parseArgv(int argc, char *argv[]) {
break;
}
break;
}
case 'C':
options.useColorCurve = true;
break;
case 'c':
localOptions.externalPalSpec = nullptr; // Allow overriding a previous pal spec
if (musl_optarg[0] == '#') {
@@ -346,6 +329,11 @@ static char *parseArgv(int argc, char *argv[]) {
} else if (strcasecmp(musl_optarg, "embedded") == 0) {
// Use PLTE, error out if missing
options.palSpecType = Options::EMBEDDED;
} else if (strcasecmp(musl_optarg, "auto") == 0) {
options.palSpecType = Options::NO_SPEC;
} else if (strcasecmp(musl_optarg, "dmg") == 0) {
options.palSpecType = Options::DMG;
parseDmgPalSpec(0xE4); // Same darkest-first order as `sortGrayscale`
} else if (strncasecmp(musl_optarg, "dmg=", literal_strlen("dmg=")) == 0) {
options.palSpecType = Options::DMG;
parseDmgPalSpec(&musl_optarg[literal_strlen("dmg=")]);
@@ -354,46 +342,52 @@ static char *parseArgv(int argc, char *argv[]) {
localOptions.externalPalSpec = musl_optarg;
}
break;
case 'd':
options.bitDepth = parseNumber(arg, "Bit depth", 2);
if (*arg != '\0') {
error("Bit depth (-b) argument must be a valid number, not \"%s\"", musl_optarg);
error("Bit depth ('-b') argument must be a valid number, not \"%s\"", musl_optarg);
} else if (options.bitDepth != 1 && options.bitDepth != 2) {
error("Bit depth must be 1 or 2, not %" PRIu8, options.bitDepth);
options.bitDepth = 2;
}
break;
// LCOV_EXCL_START
case 'h':
usage.printAndExit(0); // LCOV_EXCL_LINE
usage.printAndExit(0);
// LCOV_EXCL_STOP
case 'i':
if (!options.inputTileset.empty()) {
warnx("Overriding input tileset file %s", options.inputTileset.c_str());
warnx("Overriding input tileset file \"%s\"", options.inputTileset.c_str());
}
options.inputTileset = musl_optarg;
break;
case 'L':
options.inputSlice.left = parseNumber(arg, "Input slice left coordinate");
if (options.inputSlice.left > INT16_MAX) {
error("Input slice left coordinate is out of range!");
break;
}
skipWhitespace(arg);
skipBlankSpace(arg);
if (*arg != ',') {
error("Missing comma after left coordinate in \"%s\"", musl_optarg);
break;
}
++arg;
skipWhitespace(arg);
skipBlankSpace(arg);
options.inputSlice.top = parseNumber(arg, "Input slice upper coordinate");
skipWhitespace(arg);
skipBlankSpace(arg);
if (*arg != ':') {
error("Missing colon after upper coordinate in \"%s\"", musl_optarg);
break;
}
++arg;
skipWhitespace(arg);
skipBlankSpace(arg);
options.inputSlice.width = parseNumber(arg, "Input slice width");
skipWhitespace(arg);
skipBlankSpace(arg);
if (options.inputSlice.width == 0) {
error("Input slice width may not be 0!");
}
@@ -402,7 +396,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
}
++arg;
skipWhitespace(arg);
skipBlankSpace(arg);
options.inputSlice.height = parseNumber(arg, "Input slice height");
if (options.inputSlice.height == 0) {
error("Input slice height may not be 0!");
@@ -411,8 +405,9 @@ static char *parseArgv(int argc, char *argv[]) {
error("Unexpected extra characters after slice spec in \"%s\"", musl_optarg);
}
break;
case 'l':
number = parseNumber(arg, "Base palette ID", 0);
case 'l': {
uint16_t number = parseNumber(arg, "Base palette ID", 0);
if (*arg != '\0') {
error("Base palette ID must be a valid number, not \"%s\"", musl_optarg);
} else if (number >= 256) {
@@ -421,13 +416,17 @@ static char *parseArgv(int argc, char *argv[]) {
options.basePalID = number;
}
break;
}
case 'm':
options.allowMirroringX = true; // Imply `-X`
options.allowMirroringY = true; // Imply `-Y`
[[fallthrough]]; // Imply `-u`
case 'u':
options.allowDedup = true;
break;
case 'N':
options.maxNbTiles[0] = parseNumber(arg, "Number of tiles in bank 0", 256);
if (options.maxNbTiles[0] > 256) {
@@ -437,7 +436,7 @@ static char *parseArgv(int argc, char *argv[]) {
options.maxNbTiles[1] = 0;
break;
}
skipWhitespace(arg);
skipBlankSpace(arg);
if (*arg != ',') {
error(
"Bank capacity must be one or two comma-separated numbers, not \"%s\"",
@@ -446,7 +445,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
}
++arg; // Skip comma
skipWhitespace(arg);
skipBlankSpace(arg);
options.maxNbTiles[1] = parseNumber(arg, "Number of tiles in bank 1", 256);
if (options.maxNbTiles[1] > 256) {
error("Bank 1 cannot contain more than 256 tiles");
@@ -459,31 +458,37 @@ static char *parseArgv(int argc, char *argv[]) {
break;
}
break;
case 'n':
number = parseNumber(arg, "Number of palettes", 256);
case 'n': {
uint16_t number = parseNumber(arg, "Number of palettes", 256);
if (*arg != '\0') {
error("Number of palettes (-n) must be a valid number, not \"%s\"", musl_optarg);
error("Number of palettes ('-n') must be a valid number, not \"%s\"", musl_optarg);
}
if (number > 256) {
error("Number of palettes (-n) must not exceed 256!");
error("Number of palettes ('-n') must not exceed 256!");
} else if (number == 0) {
error("Number of palettes (-n) may not be 0!");
error("Number of palettes ('-n') may not be 0!");
} else {
options.nbPalettes = number;
}
break;
}
case 'O':
localOptions.groupOutputs = true;
break;
case 'o':
if (!options.output.empty()) {
warnx("Overriding tile data file %s", options.output.c_str());
}
options.output = musl_optarg;
break;
case 'P':
localOptions.autoPalettes = true;
break;
case 'p':
localOptions.autoPalettes = false;
if (!options.palettes.empty()) {
@@ -491,9 +496,11 @@ static char *parseArgv(int argc, char *argv[]) {
}
options.palettes = musl_optarg;
break;
case 'Q':
localOptions.autoPalmap = true;
break;
case 'q':
localOptions.autoPalmap = false;
if (!options.palmap.empty()) {
@@ -501,27 +508,33 @@ static char *parseArgv(int argc, char *argv[]) {
}
options.palmap = musl_optarg;
break;
case 'r':
localOptions.reverse = true;
options.reversedWidth = parseNumber(arg, "Reversed image stride");
if (*arg != '\0') {
error("Reversed image stride (-r) must be a valid number, not \"%s\"", musl_optarg);
error(
"Reversed image stride ('-r') must be a valid number, not \"%s\"", musl_optarg
);
}
break;
case 's':
options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4);
if (*arg != '\0') {
error("Palette size (-s) must be a valid number, not \"%s\"", musl_optarg);
error("Palette size ('-s') must be a valid number, not \"%s\"", musl_optarg);
}
if (options.nbColorsPerPal > 4) {
error("Palette size (-s) must not exceed 4!");
error("Palette size ('-s') must not exceed 4!");
} else if (options.nbColorsPerPal == 0) {
error("Palette size (-s) may not be 0!");
error("Palette size ('-s') may not be 0!");
}
break;
case 'T':
localOptions.autoTilemap = true;
break;
case 't':
localOptions.autoTilemap = false;
if (!options.tilemap.empty()) {
@@ -529,41 +542,52 @@ static char *parseArgv(int argc, char *argv[]) {
}
options.tilemap = musl_optarg;
break;
case 'V':
// LCOV_EXCL_START
case 'V':
printf("rgbgfx %s\n", get_package_version_string());
exit(0);
// LCOV_EXCL_STOP
case 'v':
// LCOV_EXCL_START
if (options.verbosity < Options::VERB_VVVVVV) {
++options.verbosity;
}
incrementVerbosity();
break;
// LCOV_EXCL_STOP
case 'W':
warnings.processWarningFlag(musl_optarg);
break;
case 'w':
warnings.state.warningsEnabled = false;
break;
case 'x':
options.trim = parseNumber(arg, "Number of tiles to trim", 0);
if (*arg != '\0') {
error("Tile trim (-x) argument must be a valid number, not \"%s\"", musl_optarg);
error("Tile trim ('-x') argument must be a valid number, not \"%s\"", musl_optarg);
}
break;
case 'X':
options.allowMirroringX = true;
options.allowDedup = true; // Imply `-u`
break;
case 'Y':
options.allowMirroringY = true;
options.allowDedup = true; // Imply `-u`
break;
case 'Z':
options.columnMajor = true;
break;
case 0: // Long-only options
if (longOpt == 'c' && !style_Parse(musl_optarg)) {
fatal("Invalid argument for option '--color'");
}
break;
case 1: // Positional argument, requested by leading `-` in opt string
if (musl_optarg[0] == '@') {
// Instruct the caller to process that at-file
@@ -572,6 +596,7 @@ static char *parseArgv(int argc, char *argv[]) {
registerInput(musl_optarg);
}
break;
default:
usage.printAndExit(1); // LCOV_EXCL_LINE
}
@@ -580,93 +605,70 @@ static char *parseArgv(int argc, char *argv[]) {
return nullptr; // Done processing this argv
}
// LCOV_EXCL_START
static void verboseOutputConfig() {
if (!checkVerbosity(VERB_CONFIG)) {
return;
}
style_Set(stderr, STYLE_MAGENTA, false);
fprintf(stderr, "rgbgfx %s\n", get_package_version_string());
if (options.verbosity >= Options::VERB_VVVVVV) {
putc('\n', stderr);
// clang-format off: vertically align values
static std::array<uint16_t, 21> gfx{
0b0111111110,
0b1111111111,
0b1110011001,
0b1110011001,
0b1111111111,
0b1111111111,
0b1110000001,
0b1111000011,
0b0111111110,
0b0001111000,
0b0111111110,
0b1111111111,
0b1111111111,
0b1111111111,
0b1101111011,
0b1101111011,
0b0011111100,
0b0011001100,
0b0111001110,
0b0111001110,
0b0111001110,
};
// clang-format on
static std::array<char const *, 3> textbox{
" ,----------------------------------------.",
" | Augh, dimensional interference again?! |",
" `----------------------------------------'",
};
for (size_t i = 0; i < gfx.size(); ++i) {
uint16_t row = gfx[i];
for (uint8_t _ = 0; _ < 10; ++_) {
unsigned char c = row & 1 ? '0' : ' ';
putc(c, stderr);
// Double the pixel horizontally, otherwise the aspect ratio looks wrong
putc(c, stderr);
row >>= 1;
}
if (i < textbox.size()) {
fputs(textbox[i], stderr);
}
putc('\n', stderr);
}
putc('\n', stderr);
}
printVVVVVVerbosity();
fputs("Options:\n", stderr);
// -Z/--columns
if (options.columnMajor) {
fputs("\tVisit image in column-major order\n", stderr);
}
// -u/--unique-tiles
if (options.allowDedup) {
fputs("\tAllow deduplicating tiles\n", stderr);
fputs("\tAllow deduplicating identical tiles\n", stderr);
}
if (options.allowMirroringX) {
// -m/--mirror-tiles
if (options.allowMirroringX && options.allowMirroringY) {
fputs("\tAllow deduplicating mirrored tiles\n", stderr);
}
// -X/--mirror-x
else if (options.allowMirroringX) {
fputs("\tAllow deduplicating horizontally mirrored tiles\n", stderr);
}
if (options.allowMirroringY) {
// -Y/--mirror-y
else if (options.allowMirroringY) {
fputs("\tAllow deduplicating vertically mirrored tiles\n", stderr);
}
// -C/--color-curve
if (options.useColorCurve) {
fputs("\tUse color curve\n", stderr);
}
// -d/--depth
fprintf(stderr, "\tBit depth: %" PRIu8 "bpp\n", options.bitDepth);
// -x/--trim-end
if (options.trim != 0) {
fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
}
// -n/--nb-palettes
fprintf(stderr, "\tMaximum %" PRIu16 " palettes\n", options.nbPalettes);
// -s/--palette-size
fprintf(stderr, "\tPalettes contain %" PRIu8 " colors\n", options.nbColorsPerPal);
// -c/--colors
if (options.palSpecType == Options::NO_SPEC) {
fputs("\tAutomatic palette generation\n", stderr);
} else {
fprintf(stderr, "\t%s palette spec\n", [] {
switch (options.palSpecType) {
case Options::NO_SPEC:
return "No";
case Options::EXPLICIT:
return "Explicit";
case Options::EMBEDDED:
return "Embedded";
case Options::DMG:
return "DMG";
}
default:
return "???";
}
}());
}
if (options.palSpecType == Options::EXPLICIT) {
fputs("\t[\n", stderr);
for (auto const &pal : options.palSpec) {
@@ -682,6 +684,8 @@ static void verboseOutputConfig() {
}
fputs("\t]\n", stderr);
}
// -L/--slice
if (options.inputSlice.specified()) {
fprintf(
stderr,
"\tInput image slice: %" PRIu16 "x%" PRIu16 " pixels starting at (%" PRIu16 ", %" PRIu16
@@ -691,31 +695,56 @@ static void verboseOutputConfig() {
options.inputSlice.left,
options.inputSlice.top
);
}
// -b/--base-tiles
if (options.baseTileIDs[0] || options.baseTileIDs[1]) {
fprintf(
stderr,
"\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n",
"\tBase tile IDs: bank 0 = 0x%02" PRIx8 ", bank 1 = 0x%02" PRIx8 "\n",
options.baseTileIDs[0],
options.baseTileIDs[1]
);
}
// -l/--base-palette
if (options.basePalID) {
fprintf(stderr, "\tBase palette ID: %" PRIu8 "\n", options.basePalID);
}
// -N/--nb-tiles
fprintf(
stderr,
"\tMaximum %" PRIu16 " tiles in bank 0, %" PRIu16 " in bank 1\n",
"\tMaximum %" PRIu16 " tiles in bank 0, and %" PRIu16 " in bank 1\n",
options.maxNbTiles[0],
options.maxNbTiles[1]
);
// -O/--group-outputs (influences other options)
auto printPath = [](char const *name, std::string const &path) {
if (!path.empty()) {
fprintf(stderr, "\t%s: %s\n", name, path.c_str());
}
};
// file
printPath("Input image", options.input);
// -i/--input-tileset
printPath("Input tileset", options.inputTileset);
// -o/--output
printPath("Output tile data", options.output);
// -t/--tilemap or -T/--auto-tilemap
printPath("Output tilemap", options.tilemap);
// -a/--attrmap or -A/--auto-attrmap
printPath("Output attrmap", options.attrmap);
// -p/--palette or -P/--auto-palette
printPath("Output palettes", options.palettes);
fputs("Ready.\n", stderr);
// -q/--palette-map or -Q/--auto-palette-map
printPath("Output palette map", options.palmap);
// -r/--reverse
if (localOptions.reverse) {
fprintf(stderr, "\tReverse image width: %" PRIu16 " tiles\n", options.reversedWidth);
}
fputs("Ready.\n", stderr);
style_Reset(stderr);
}
// LCOV_EXCL_STOP
// Manual implementation of std::filesystem::path.replace_extension().
// macOS <10.15 did not support std::filesystem::path.
@@ -750,6 +779,7 @@ int main(int argc, char *argv[]) {
};
std::vector<AtFileStackEntry> atFileStack;
// Parse CLI options
int curArgc = argc;
char **curArgv = argv;
std::vector<std::vector<char>> argPools;
@@ -839,11 +869,7 @@ int main(int argc, char *argv[]) {
parseExternalPalSpec(localOptions.externalPalSpec);
}
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_CFG) {
verboseOutputConfig();
}
// LCOV_EXCL_STOP
verboseOutputConfig(); // LCOV_EXCL_LINE
// Do not do anything if option parsing went wrong.
requireZeroErrors();
@@ -858,7 +884,7 @@ int main(int argc, char *argv[]) {
&& !localOptions.reverse) {
processPalettes();
} else {
usage.printAndExit("No input image specified");
usage.printAndExit("No input file specified (pass \"-\" to read from standard input)");
}
requireZeroErrors();

View File

@@ -5,15 +5,20 @@
#include <algorithm>
#include <deque>
#include <inttypes.h>
#include <iterator>
#include <numeric>
#include <optional>
#include <queue>
#include <stdint.h>
#include <stdio.h>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>
#include "helpers.hpp"
#include "style.hpp"
#include "verbosity.hpp"
#include "gfx/color_set.hpp"
#include "gfx/main.hpp"
@@ -247,6 +252,11 @@ public:
static void verboseOutputAssignments(
std::vector<AssignedSets> const &assignments, std::vector<ColorSet> const &colorSets
) {
if (!checkVerbosity(VERB_INFO)) {
return;
}
style_Set(stderr, STYLE_MAGENTA, false);
for (AssignedSets const &assignment : assignments) {
fputs("{ ", stderr);
for (ColorSetAttrs const &attrs : assignment) {
@@ -257,6 +267,7 @@ static void verboseOutputAssignments(
}
fprintf(stderr, "} (volume = %zu)\n", assignment.volume());
}
style_Reset(stderr);
}
// LCOV_EXCL_STOP
@@ -284,9 +295,7 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
}
};
options.verbosePrint(
Options::VERB_DEBUG, "%zu palettes before decanting\n", assignments.size()
);
verbosePrint(VERB_DEBUG, "%zu palettes before decanting\n", assignments.size());
// Decant on palettes
decantOn([&colorSets](AssignedSets &to, AssignedSets &from) {
@@ -298,9 +307,7 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
from.clear();
}
});
options.verbosePrint(
Options::VERB_DEBUG, "%zu palettes after decanting on palettes\n", assignments.size()
);
verbosePrint(VERB_DEBUG, "%zu palettes after decanting on palettes\n", assignments.size());
// Decant on "components" (color sets sharing colors)
decantOn([&colorSets](AssignedSets &to, AssignedSets &from) {
@@ -344,8 +351,8 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
}
}
});
options.verbosePrint(
Options::VERB_DEBUG, "%zu palettes after decanting on \"components\"\n", assignments.size()
verbosePrint(
VERB_DEBUG, "%zu palettes after decanting on \"components\"\n", assignments.size()
);
// Decant on individual color sets
@@ -357,15 +364,11 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
}
}
});
options.verbosePrint(
Options::VERB_DEBUG, "%zu palettes after decanting on color sets\n", assignments.size()
);
verbosePrint(VERB_DEBUG, "%zu palettes after decanting on color sets\n", assignments.size());
}
std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet> const &colorSets) {
options.verbosePrint(
Options::VERB_LOG_ACT, "Paginating palettes using \"overload-and-remove\" strategy...\n"
);
std::pair<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet> const &colorSets) {
verbosePrint(VERB_NOTICE, "Paginating palettes using \"overload-and-remove\" strategy...\n");
// Sort the color sets by size, which improves the packing algorithm's efficiency
auto const indexOfLargestColorSetFirst = [&colorSets](size_t left, size_t right) {
@@ -389,7 +392,7 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
!queue.empty();
queue.pop()) {
ColorSetAttrs const &attrs = queue.front(); // Valid until the `queue.pop()`
options.verbosePrint(Options::VERB_TRACE, "Handling color set %zu\n", attrs.colorSetIndex);
verbosePrint(VERB_TRACE, "Handling color set %zu\n", attrs.colorSetIndex);
ColorSet const &colorSet = colorSets[attrs.colorSetIndex];
size_t bestPalIndex = assignments.size();
@@ -404,8 +407,8 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
}
uint32_t relSize = assignments[i].relSizeOf(colorSet);
options.verbosePrint(
Options::VERB_TRACE,
verbosePrint(
VERB_TRACE,
" Relative size to palette %zu (of %zu): %" PRIu32 " (size = %zu)\n",
i,
assignments.size(),
@@ -420,8 +423,8 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
if (bestPalIndex == assignments.size()) {
// Found nowhere to put it, create a new page containing just that one
options.verbosePrint(
Options::VERB_TRACE,
verbosePrint(
VERB_TRACE,
"Assigning color set %zu to new palette %zu\n",
attrs.colorSetIndex,
bestPalIndex
@@ -430,8 +433,8 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
continue;
}
options.verbosePrint(
Options::VERB_TRACE,
verbosePrint(
VERB_TRACE,
"Assigning color set %zu to palette %zu\n",
attrs.colorSetIndex,
bestPalIndex
@@ -448,8 +451,8 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
uint32_t relSize1 = bestPal.relSizeOf(colorSet1);
uint32_t relSize2 = bestPal.relSizeOf(colorSet2);
options.verbosePrint(
Options::VERB_TRACE,
verbosePrint(
VERB_TRACE,
" Color sets %zu <=> %zu: Efficiency: %zu / %" PRIu32 " <=> %zu / "
"%" PRIu32 "\n",
attrs1.colorSetIndex,
@@ -470,8 +473,8 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
// If this overloads the palette, get it back to normal (if possible)
while (bestPal.volume() > options.maxOpaqueColors()) {
options.verbosePrint(
Options::VERB_TRACE,
verbosePrint(
VERB_TRACE,
"Palette %zu is overloaded! (%zu > %" PRIu8 ")\n",
bestPalIndex,
bestPal.volume(),
@@ -488,13 +491,13 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
// All efficiencies are identical iff min equals max
if (compareEfficiency(*minEfficiencyIter, *maxEfficiencyIter) == 0) {
options.verbosePrint(Options::VERB_TRACE, " All efficiencies are identical\n");
verbosePrint(VERB_TRACE, " All efficiencies are identical\n");
break;
}
// Remove the color set with minimal efficiency
options.verbosePrint(
Options::VERB_TRACE, " Removing color set %zu\n", minEfficiencyIter->colorSetIndex
verbosePrint(
VERB_TRACE, " Removing color set %zu\n", minEfficiencyIter->colorSetIndex
);
queue.emplace(std::move(*minEfficiencyIter));
queue.back().banFrom(bestPalIndex); // Ban it from this palette
@@ -526,16 +529,16 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
return pal.canFit(colorSet);
});
if (iter == assignments.end()) { // No such page, create a new one
options.verbosePrint(
Options::VERB_DEBUG,
verbosePrint(
VERB_DEBUG,
"Adding new palette (%zu) for overflowing color set %zu\n",
assignments.size(),
attrs.colorSetIndex
);
assignments.emplace_back(colorSets, std::move(attrs));
} else {
options.verbosePrint(
Options::VERB_DEBUG,
verbosePrint(
VERB_DEBUG,
"Assigning overflowing color set %zu to palette %zu\n",
attrs.colorSetIndex,
iter - assignments.begin()
@@ -544,21 +547,13 @@ std::tuple<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet>
}
}
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
verboseOutputAssignments(assignments, colorSets);
}
// LCOV_EXCL_STOP
verboseOutputAssignments(assignments, colorSets); // LCOV_EXCL_LINE
// "Decant" the result
decant(assignments, colorSets);
// Note that the result does not contain any empty palettes
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
verboseOutputAssignments(assignments, colorSets);
}
// LCOV_EXCL_STOP
verboseOutputAssignments(assignments, colorSets); // LCOV_EXCL_LINE
std::vector<size_t> mappings(colorSets.size());
for (size_t i = 0; i < assignments.size(); ++i) {

View File

@@ -3,13 +3,19 @@
#include "gfx/pal_sorting.hpp"
#include <algorithm>
#include <array>
#include <optional>
#include <stdint.h>
#include <vector>
#include "helpers.hpp"
#include "verbosity.hpp"
#include "gfx/main.hpp"
#include "gfx/rgba.hpp"
void sortIndexed(std::vector<Palette> &palettes, std::vector<Rgba> const &embPal) {
options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes using embedded palette...\n");
verbosePrint(VERB_NOTICE, "Sorting palettes using embedded palette...\n");
for (Palette &pal : palettes) {
std::sort(RANGE(pal), [&](uint16_t lhs, uint16_t rhs) {
@@ -35,7 +41,7 @@ void sortIndexed(std::vector<Palette> &palettes, std::vector<Rgba> const &embPal
void sortGrayscale(
std::vector<Palette> &palettes, std::array<std::optional<Rgba>, NB_COLOR_SLOTS> const &colors
) {
options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palette by grayscale bins...\n");
verbosePrint(VERB_NOTICE, "Sorting palette by grayscale bins...\n");
// This method is only applicable if there are at most as many colors as colors per palette, so
// we should only have a single palette.
@@ -59,7 +65,7 @@ static unsigned int luminance(uint16_t color) {
}
void sortRgb(std::vector<Palette> &palettes) {
options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes by luminance...\n");
verbosePrint(VERB_NOTICE, "Sorting palettes by luminance...\n");
for (Palette &pal : palettes) {
// Sort from lightest to darkest

View File

@@ -3,27 +3,29 @@
#include "gfx/pal_spec.hpp"
#include <algorithm>
#include <array>
#include <charconv>
#include <errno.h>
#include <fstream>
#include <inttypes.h>
#include <limits.h>
#include <ios>
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <streambuf>
#include <string.h>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "diagnostics.hpp"
#include "helpers.hpp"
#include "platform.hpp"
#include "util.hpp" // UpperMap
#include "util.hpp" // UpperMap, isDigit
#include "gfx/main.hpp"
#include "gfx/png.hpp"
#include "gfx/rgba.hpp"
#include "gfx/warning.hpp"
using namespace std::string_view_literals;
@@ -31,7 +33,7 @@ using namespace std::string_view_literals;
static char const *hexDigits = "0123456789ABCDEFabcdef";
template<typename Str> // Should be std::string or std::string_view
static void skipWhitespace(Str const &str, size_t &pos) {
static void skipBlankSpace(Str const &str, size_t &pos) {
pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length());
}
@@ -43,7 +45,7 @@ static constexpr uint8_t nibble(char c) {
assume(c <= 'F');
return c - 'A' + 10;
} else {
assume(c >= '0' && c <= '9');
assume(isDigit(c));
return c - '0';
}
}
@@ -70,13 +72,11 @@ void parseInlinePalSpec(char const * const rawArg) {
error("%s", msg); // `format_` and `-Wformat-security` would complain about `error(msg);`
fprintf(
stderr,
"In inline palette spec: %s\n"
" ",
rawArg
"In inline palette spec: \"%s\"\n%*c",
rawArg,
static_cast<int>(literal_strlen("In inline palette spec: \"") + ofs),
' '
);
for (size_t i = ofs; i; --i) {
putc(' ', stderr);
}
for (size_t i = len; i; --i) {
putc('^', stderr);
}
@@ -122,8 +122,8 @@ void parseInlinePalSpec(char const * const rawArg) {
n = pos;
}
// Skip whitespace, if any
skipWhitespace(arg, n);
// Skip trailing space, if any
skipBlankSpace(arg, n);
// Skip comma/semicolon, or end
if (n == arg.length()) {
@@ -136,7 +136,7 @@ void parseInlinePalSpec(char const * const rawArg) {
++nbColors;
// A trailing comma may be followed by a semicolon
skipWhitespace(arg, n);
skipBlankSpace(arg, n);
if (n == arg.length()) {
break;
} else if (arg[n] != ';' && arg[n] != ':') {
@@ -151,7 +151,7 @@ void parseInlinePalSpec(char const * const rawArg) {
case ':':
case ';':
++n;
skipWhitespace(arg, n);
skipBlankSpace(arg, n);
nbColors = 0; // Start a new palette
// Avoid creating a spurious empty palette
@@ -255,7 +255,7 @@ static std::optional<Rgba> parseColor(std::string const &str, size_t &n, uint16_
error("Failed to parse color #%d (\"%s\"): invalid red component", i + 1, str.c_str());
return std::nullopt;
}
skipWhitespace(str, n);
skipBlankSpace(str, n);
if (n == str.length()) {
error("Failed to parse color #%d (\"%s\"): missing green component", i + 1, str.c_str());
return std::nullopt;
@@ -265,7 +265,7 @@ static std::optional<Rgba> parseColor(std::string const &str, size_t &n, uint16_
error("Failed to parse color #%d (\"%s\"): invalid green component", i + 1, str.c_str());
return std::nullopt;
}
skipWhitespace(str, n);
skipBlankSpace(str, n);
if (n == str.length()) {
error("Failed to parse color #%d (\"%s\"): missing blue component", i + 1, str.c_str());
return std::nullopt;
@@ -300,7 +300,7 @@ static void parsePSPFile(char const *filename, std::filebuf &file) {
size_t n = 0;
std::optional<uint16_t> nbColors = parseDec<uint16_t>(line, n);
if (!nbColors || n != line.length()) {
error("Invalid \"number of colors\" line in PSP file (%s)", line.c_str());
error("Invalid \"number of colors\" line in PSP file (\"%s\")", line.c_str());
return;
}
@@ -358,7 +358,7 @@ static void parseGPLFile(char const *filename, std::filebuf &file) {
}
size_t n = 0;
skipWhitespace(line, n);
skipBlankSpace(line, n);
// Skip empty lines, or lines that contain just a comment.
if (line.length() == n || line[n] == '#') {
continue;
@@ -651,7 +651,7 @@ void parseExternalPalSpec(char const *arg) {
// Split both parts, error out if malformed
char const *ptr = strchr(arg, ':');
if (ptr == nullptr) {
error("External palette spec must have format `fmt:path` (missing colon)");
error("External palette spec must have format \"fmt:path\" (missing colon)");
return;
}
char const *path = ptr + 1;
@@ -692,7 +692,11 @@ void parseDmgPalSpec(char const * const rawArg) {
return;
}
options.palSpecDmg = toHex(arg[0], arg[1]);
parseDmgPalSpec(toHex(arg[0], arg[1]));
}
void parseDmgPalSpec(uint8_t palSpecDmg) {
options.palSpecDmg = palSpecDmg;
// Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex`
for (uint8_t i = 0; i < 4; ++i) {
@@ -717,7 +721,7 @@ void parseBackgroundPalSpec(char const *arg) {
}
if (arg[0] != '#') {
error("Background color specification must be `#rgb`, `#rrggbb`, or `transparent`");
error("Background color specification must be \"#rgb\", \"#rrggbb\", or \"transparent\"");
return;
}

View File

@@ -2,9 +2,23 @@
#include "gfx/png.hpp"
#include <array>
#include <errno.h>
#include <inttypes.h>
#include <ios>
#include <png.h>
#include <pngconf.h>
#include <stdint.h>
#include <stdio.h>
#include <streambuf>
#include <string.h>
#include <vector>
#include "diagnostics.hpp"
#include "helpers.hpp"
#include "style.hpp"
#include "verbosity.hpp"
#include "gfx/main.hpp"
#include "gfx/rgba.hpp"
#include "gfx/warning.hpp"
@@ -47,7 +61,7 @@ static void readData(png_structp png, png_bytep data, size_t length) {
Png::Png(char const *filename, std::streambuf &file) {
Input input(filename, file);
options.verbosePrint(Options::VERB_LOG_ACT, "Reading PNG file \"%s\"\n", input.filename);
verbosePrint(VERB_NOTICE, "Reading PNG file \"%s\"\n", input.filename);
std::array<unsigned char, 8> pngHeader;
if (input.file.sgetn(reinterpret_cast<char *>(pngHeader.data()), pngHeader.size())
@@ -56,7 +70,7 @@ Png::Png(char const *filename, std::streambuf &file) {
fatal("File \"%s\" is not a valid PNG image", input.filename); // LCOV_EXCL_LINE
}
options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n");
verbosePrint(VERB_INFO, "PNG header signature is OK\n");
png_structp png = png_create_read_struct(
PNG_LIBPNG_VER_STRING, static_cast<png_voidp>(&input), handleError, handleWarning
@@ -110,8 +124,8 @@ Png::Png(char const *filename, std::streambuf &file) {
return "unknown interlace type";
}
};
options.verbosePrint(
Options::VERB_INTERM,
verbosePrint(
VERB_INFO,
"PNG image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n",
width,
height,
@@ -139,18 +153,17 @@ Png::Png(char const *filename, std::streambuf &file) {
);
}
options.verbosePrint(
Options::VERB_INTERM, "Embedded PNG palette has %d colors: [", nbColors
);
if (checkVerbosity(VERB_INFO)) {
style_Set(stderr, STYLE_MAGENTA, false);
fprintf(stderr, "Embedded PNG palette has %d colors: [", nbColors);
for (int i = 0; i < nbColors; ++i) {
if (i > 0) {
options.verbosePrint(Options::VERB_INTERM, ", ");
fprintf(stderr, "%s#%08x", i > 0 ? ", " : "", palette[i].toCSS());
}
options.verbosePrint(Options::VERB_INTERM, "#%08x", palette[i].toCSS());
fprintf(stderr, "]\n");
style_Reset(stderr);
}
options.verbosePrint(Options::VERB_INTERM, "]\n");
} else {
options.verbosePrint(Options::VERB_INTERM, "No embedded PNG palette\n");
verbosePrint(VERB_INFO, "No embedded PNG palette\n");
}
// Set up transformations to turn everything into RGBA8888 for simplicity of handling

View File

@@ -3,13 +3,16 @@
#include "gfx/process.hpp"
#include <algorithm>
#include <array>
#include <errno.h>
#include <inttypes.h>
#include <ios>
#include <optional>
#include <png.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <tuple>
#include <unordered_set>
#include <utility>
@@ -19,12 +22,15 @@
#include "file.hpp"
#include "helpers.hpp"
#include "itertools.hpp"
#include "style.hpp"
#include "verbosity.hpp"
#include "gfx/color_set.hpp"
#include "gfx/main.hpp"
#include "gfx/pal_packing.hpp"
#include "gfx/pal_sorting.hpp"
#include "gfx/png.hpp"
#include "gfx/rgba.hpp"
#include "gfx/warning.hpp"
static bool isBgColorTransparent() {
@@ -79,8 +85,8 @@ struct Image {
bool isSuitableForGrayscale() const {
// Check that all of the grays don't fall into the same "bin"
if (colors.size() > options.maxOpaqueColors()) { // Apply the Pigeonhole Principle
options.verbosePrint(
Options::VERB_DEBUG,
verbosePrint(
VERB_DEBUG,
"Too many colors for grayscale sorting (%zu > %" PRIu8 ")\n",
colors.size(),
options.maxOpaqueColors()
@@ -93,8 +99,8 @@ struct Image {
continue;
}
if (!color->isGray()) {
options.verbosePrint(
Options::VERB_DEBUG,
verbosePrint(
VERB_DEBUG,
"Found non-gray color #%08x, not using grayscale sorting\n",
color->toCSS()
);
@@ -102,8 +108,8 @@ struct Image {
}
uint8_t mask = 1 << color->grayIndex();
if (bins & mask) { // Two in the same bin!
options.verbosePrint(
Options::VERB_DEBUG,
verbosePrint(
VERB_DEBUG,
"Color #%08x conflicts with another one, not using grayscale sorting\n",
color->toCSS()
);
@@ -311,7 +317,7 @@ static void generatePalSpec(Image const &image) {
// Generate a palette spec from the first few colors in the embedded palette
std::vector<Rgba> const &embPal = image.png.palette;
if (embPal.empty()) {
fatal("`-c embedded` was given, but the PNG does not have an embedded palette!");
fatal("\"-c embedded\" was given, but the PNG does not have an embedded palette!");
}
// Ignore extraneous colors if they are unused
@@ -329,20 +335,22 @@ static void generatePalSpec(Image const &image) {
}
}
static std::tuple<std::vector<size_t>, std::vector<Palette>>
static std::pair<std::vector<size_t>, std::vector<Palette>>
generatePalettes(std::vector<ColorSet> const &colorSets, Image const &image) {
// Run a "pagination" problem solver
auto [mappings, nbPalettes] = overloadAndRemove(colorSets);
assume(mappings.size() == colorSets.size());
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
if (checkVerbosity(VERB_INFO)) {
style_Set(stderr, STYLE_MAGENTA, false);
fprintf(
stderr, "Color set mappings: (%zu palette%s)\n", nbPalettes, nbPalettes != 1 ? "s" : ""
);
for (size_t i = 0; i < mappings.size(); ++i) {
fprintf(stderr, "%zu -> %zu\n", i, mappings[i]);
}
style_Reset(stderr);
}
// LCOV_EXCL_STOP
@@ -379,7 +387,7 @@ static std::tuple<std::vector<size_t>, std::vector<Palette>>
return {mappings, palettes};
}
static std::tuple<std::vector<size_t>, std::vector<Palette>>
static std::pair<std::vector<size_t>, std::vector<Palette>>
makePalsAsSpecified(std::vector<ColorSet> const &colorSets) {
// Convert the palette spec to actual palettes
std::vector<Palette> palettes(options.palSpec.size());
@@ -438,7 +446,8 @@ static std::tuple<std::vector<size_t>, std::vector<Palette>>
static void outputPalettes(std::vector<Palette> const &palettes) {
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
if (checkVerbosity(VERB_INFO)) {
style_Set(stderr, STYLE_MAGENTA, false);
for (Palette const &palette : palettes) {
fputs("{ ", stderr);
for (uint16_t colorIndex : palette) {
@@ -446,6 +455,7 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
}
fputs("}\n", stderr);
}
style_Reset(stderr);
}
// LCOV_EXCL_STOP
@@ -606,7 +616,7 @@ public:
template<>
struct std::hash<TileData> {
std::size_t operator()(TileData const &tile) const { return tile.hash(); }
size_t operator()(TileData const &tile) const { return tile.hash(); }
};
static void outputUnoptimizedTileData(
@@ -629,7 +639,7 @@ static void outputUnoptimizedTileData(
uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
uint64_t tileIdx = 0;
for (auto [tile, attr] : zip(image.visitAsTiles(), attrmap)) {
for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) {
// Do not emit fully-background tiles.
if (attr.isBackgroundTile()) {
++tileIdx;
@@ -721,19 +731,15 @@ struct UniqueTiles {
UniqueTiles(UniqueTiles &&) = default;
// Adds a tile to the collection, and returns its ID
std::tuple<uint16_t, TileData::MatchType> addTile(TileData newTile) {
auto [tileData, inserted] = tileset.insert(newTile);
TileData::MatchType matchType = TileData::NOPE;
if (inserted) {
std::pair<uint16_t, TileData::MatchType> addTile(TileData newTile) {
if (auto [tileData, inserted] = tileset.insert(newTile); inserted) {
// Give the new tile the next available unique ID
tileData->tileID = static_cast<uint16_t>(tiles.size());
// Pointers are never invalidated!
tiles.emplace_back(&*tileData);
tiles.emplace_back(&*tileData); // Pointers are never invalidated!
return {tileData->tileID, TileData::NOPE};
} else {
matchType = tileData->tryMatching(newTile);
return {tileData->tileID, tileData->tryMatching(newTile)};
}
return {tileData->tileID, matchType};
}
size_t size() const { return tiles.size(); }
@@ -789,7 +795,7 @@ static UniqueTiles dedupTiles(
if (matchType != TileData::NOPE) {
error(
"The input tileset's tile #%hu was deduplicated; please check that your "
"deduplication flags (`-u`, `-m`) are consistent with what was used to "
"deduplication flags ('-u', '-m') are consistent with what was used to "
"generate the input tileset",
tileID
);
@@ -798,7 +804,7 @@ static UniqueTiles dedupTiles(
}
bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty();
for (auto [tile, attr] : zip(image.visitAsTiles(), attrmap)) {
for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) {
if (attr.isBackgroundTile()) {
attr.xFlip = false;
attr.yFlip = false;
@@ -810,7 +816,7 @@ static UniqueTiles dedupTiles(
if (inputWithoutOutput && matchType == TileData::NOPE) {
error(
"Tile at (%" PRIu32 ", %" PRIu32
") is not within the input tileset, and `-o` was not given!",
") is not within the input tileset, and '-o' was not given!",
tile.x,
tile.y
);
@@ -897,7 +903,7 @@ static void
}
void processPalettes() {
options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
verbosePrint(VERB_CONFIG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
std::vector<ColorSet> colorSets;
std::vector<Palette> palettes;
@@ -907,13 +913,14 @@ void processPalettes() {
}
void process() {
options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
verbosePrint(VERB_CONFIG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
verbosePrint(VERB_NOTICE, "Reading tiles...\n");
Image image(options.input); // This also sets `hasTransparentPixels` as a side effect
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
if (checkVerbosity(VERB_INFO)) {
style_Set(stderr, STYLE_MAGENTA, false);
fputs("Image colors: [ ", stderr);
for (std::optional<Rgba> const &slot : image.colors) {
if (!slot.has_value()) {
@@ -922,6 +929,7 @@ void process() {
fprintf(stderr, "#%08x, ", slot->toCSS());
}
fputs("]\n", stderr);
style_Reset(stderr);
}
// LCOV_EXCL_STOP
@@ -1025,14 +1033,15 @@ void process() {
continue_visiting_tiles:;
}
options.verbosePrint(
Options::VERB_INTERM,
verbosePrint(
VERB_INFO,
"Image contains %zu color set%s\n",
colorSets.size(),
colorSets.size() != 1 ? "s" : ""
);
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
if (checkVerbosity(VERB_INFO)) {
style_Set(stderr, STYLE_MAGENTA, false);
for (ColorSet const &colorSet : colorSets) {
fputs("[ ", stderr);
for (uint16_t color : colorSet) {
@@ -1040,6 +1049,7 @@ continue_visiting_tiles:;
}
fputs("]\n", stderr);
}
style_Reset(stderr);
}
// LCOV_EXCL_STOP
@@ -1069,25 +1079,23 @@ continue_visiting_tiles:;
// I currently cannot figure out useful semantics for this combination of flags.
if (!options.inputTileset.empty()) {
fatal("Input tilesets are not supported without `-u`\nPlease consider explaining your "
"use case to RGBDS' developers!");
fatal("Input tilesets are not supported without '-u'");
}
if (!options.output.empty()) {
options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n");
verbosePrint(VERB_NOTICE, "Generating unoptimized tile data...\n");
outputUnoptimizedTileData(image, attrmap, palettes, mappings);
}
if (!options.tilemap.empty() || !options.attrmap.empty() || !options.palmap.empty()) {
options.verbosePrint(
Options::VERB_LOG_ACT,
"Generating unoptimized tilemap and/or attrmap and/or palmap...\n"
verbosePrint(
VERB_NOTICE, "Generating unoptimized tilemap and/or attrmap and/or palmap...\n"
);
outputUnoptimizedMaps(attrmap, mappings);
}
} else {
// All of these require the deduplication process to be performed to be output
options.verbosePrint(Options::VERB_LOG_ACT, "Deduplicating tiles...\n");
verbosePrint(VERB_NOTICE, "Deduplicating tiles...\n");
UniqueTiles tiles = dedupTiles(image, attrmap, palettes, mappings);
if (size_t nbTiles = tiles.size();
@@ -1101,22 +1109,22 @@ continue_visiting_tiles:;
}
if (!options.output.empty()) {
options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized tile data...\n");
verbosePrint(VERB_NOTICE, "Generating optimized tile data...\n");
outputTileData(tiles);
}
if (!options.tilemap.empty()) {
options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized tilemap...\n");
verbosePrint(VERB_NOTICE, "Generating optimized tilemap...\n");
outputTilemap(attrmap);
}
if (!options.attrmap.empty()) {
options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized attrmap...\n");
verbosePrint(VERB_NOTICE, "Generating optimized attrmap...\n");
outputAttrmap(attrmap, mappings);
}
if (!options.palmap.empty()) {
options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized palmap...\n");
verbosePrint(VERB_NOTICE, "Generating optimized palmap...\n");
outputPalmap(attrmap, mappings);
}
}

View File

@@ -6,17 +6,25 @@
#include <array>
#include <errno.h>
#include <inttypes.h>
#include <ios>
#include <math.h>
#include <optional>
#include <png.h>
#include <pngconf.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <utility>
#include <vector>
#include "diagnostics.hpp"
#include "file.hpp"
#include "helpers.hpp" // assume
#include "verbosity.hpp"
#include "gfx/main.hpp"
#include "gfx/rgba.hpp"
#include "gfx/warning.hpp"
static std::vector<uint8_t> readInto(std::string const &path) {
@@ -98,7 +106,7 @@ static void printPalette(std::array<std::optional<Rgba>, 4> const &palette) {
}
void reverse() {
options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
verbosePrint(VERB_CONFIG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
// Check for weird flag combinations
@@ -121,13 +129,13 @@ void reverse() {
if (options.inputSlice.width != 0 && options.inputSlice.width != options.reversedWidth * 8) {
warnx(
"Specified input slice width (%" PRIu16
") doesn't match provided reversing width (%" PRIu16 " * 8)",
") does not match provided reversing width (%" PRIu16 " * 8)",
options.inputSlice.width,
options.reversedWidth
);
}
options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
verbosePrint(VERB_NOTICE, "Reading tiles...\n");
std::vector<uint8_t> const tiles = readInto(options.output);
uint8_t tileSize = 8 * options.bitDepth;
if (tiles.size() % tileSize != 0) {
@@ -140,13 +148,13 @@ void reverse() {
// By default, assume tiles are not deduplicated, and add the (allegedly) trimmed tiles
size_t const nbTiles = tiles.size() / tileSize;
options.verbosePrint(Options::VERB_INTERM, "Read %zu tiles.\n", nbTiles);
verbosePrint(VERB_INFO, "Read %zu tiles.\n", nbTiles);
size_t mapSize = nbTiles + options.trim; // Image size in tiles
std::optional<std::vector<uint8_t>> tilemap;
if (!options.tilemap.empty()) {
tilemap = readInto(options.tilemap);
mapSize = tilemap->size();
options.verbosePrint(Options::VERB_INTERM, "Read %zu tilemap entries.\n", mapSize);
verbosePrint(VERB_INFO, "Read %zu tilemap entries.\n", mapSize);
}
if (mapSize == 0) {
@@ -172,13 +180,13 @@ void reverse() {
break;
}
}
options.verbosePrint(Options::VERB_INTERM, "Picked reversing width of %zu tiles\n", width);
verbosePrint(VERB_INFO, "Picked reversing width of %zu tiles\n", width);
}
if (mapSize % width != 0) {
if (options.trim == 0 && !tilemap) {
fatal(
"Total number of tiles (%zu) cannot be divided by image width (%zu tiles)\n"
"(To proceed anyway with this image width, try passing `-x %zu`)",
"(To proceed anyway with this image width, try passing \"-x %zu\")",
mapSize,
width,
width - mapSize % width
@@ -192,9 +200,7 @@ void reverse() {
}
height = mapSize / width;
options.verbosePrint(
Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width, height
);
verbosePrint(VERB_INFO, "Reversed image dimensions: %zux%zu tiles\n", width, height);
Rgba const grayColors[4] = {
Rgba(0xFFFFFFFF), Rgba(0xAAAAAAFF), Rgba(0x555555FF), Rgba(0x000000FF)
@@ -243,9 +249,9 @@ void reverse() {
}
if (options.palSpecType == Options::EXPLICIT && palettes != options.palSpec) {
warnx("Colors in the palette file do not match those specified with `-c`!");
warnx("Colors in the palette file do not match those specified with '-c'!");
// This spacing aligns "...versus with `-c`" above the column of `-c` palettes
fputs("Colors specified in the palette file: ...versus with `-c`:\n", stderr);
fputs("Colors specified in the palette file: ...versus with '-c':\n", stderr);
for (size_t i = 0; i < palettes.size() && i < options.palSpec.size(); ++i) {
if (i < palettes.size()) {
printPalette(palettes[i]);
@@ -276,7 +282,7 @@ void reverse() {
attrmap = readInto(options.attrmap);
if (attrmap->size() != mapSize) {
fatal(
"Attribute map size (%zu tiles) doesn't match image's (%zu)",
"Attribute map size (%zu tiles) does not match image size (%zu tiles)",
attrmap->size(),
mapSize
);
@@ -320,8 +326,8 @@ void reverse() {
}
}
options.verbosePrint(
Options::VERB_INTERM,
verbosePrint(
VERB_INFO,
"Number of tiles in bank {0: %" PRIu16 ", 1: %" PRIu16 "}\n",
nbTilesInBank[0],
nbTilesInBank[1]
@@ -397,14 +403,14 @@ void reverse() {
palmap = readInto(options.palmap);
if (palmap->size() != mapSize) {
fatal(
"Palette map size (%zu tiles) doesn't match image size (%zu)",
"Palette map size (%zu tiles) does not match image size (%zu tiles)",
palmap->size(),
mapSize
);
}
}
options.verbosePrint(Options::VERB_LOG_ACT, "Writing image...\n");
verbosePrint(VERB_NOTICE, "Writing image...\n");
File pngFile;
if (!pngFile.open(options.input, std::ios::out | std::ios::binary)) {
// LCOV_EXCL_START

View File

@@ -3,6 +3,7 @@
#include "gfx/rgba.hpp"
#include <algorithm>
#include <array>
#include <math.h>
#include <stdint.h>

View File

@@ -2,11 +2,14 @@
#include "gfx/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 = {
@@ -15,6 +18,7 @@ Diagnostics<WarningLevel, WarningID> warnings = {
},
.warningFlags = {
{"embedded", LEVEL_EVERYTHING},
{"obsolete", LEVEL_DEFAULT },
{"trim-nonempty", LEVEL_ALL },
},
.paramWarnings = {},
@@ -25,12 +29,14 @@ Diagnostics<WarningLevel, WarningID> warnings = {
[[noreturn]]
void giveUp() {
style_Set(stderr, STYLE_RED, true);
fprintf(
stderr,
"Conversion aborted after %" PRIu64 " error%s\n",
warnings.nbErrors,
warnings.nbErrors == 1 ? "" : "s"
);
style_Reset(stderr);
exit(1);
}
@@ -42,7 +48,9 @@ void requireZeroErrors() {
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);
@@ -54,7 +62,9 @@ void error(char const *fmt, ...) {
[[noreturn]]
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);
@@ -73,19 +83,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_YELLOW, true);
fprintf(stderr, " [-Werror=%s]\n", flag);
style_Reset(stderr);
warnings.incrementErrors();
break;

View File

@@ -4,16 +4,18 @@
#include <deque>
#include <inttypes.h>
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>
#include "diagnostics.hpp"
#include "helpers.hpp"
#include "itertools.hpp"
#include "linkdefs.hpp"
#include "platform.hpp"
#include "verbosity.hpp"
#include "link/main.hpp"
#include "link/output.hpp"
@@ -37,7 +39,7 @@ static std::vector<std::deque<FreeSpace>> memory[SECTTYPE_INVALID];
// Init the free space-modelling structs
static void initFreeSpace() {
for (SectionType type : EnumSeq(SECTTYPE_INVALID)) {
memory[type].resize(nbbanks(type));
memory[type].resize(sectTypeBanks(type));
for (std::deque<FreeSpace> &bankMem : memory[type]) {
bankMem.push_back({
.address = sectionTypeInfo[type].startAddr,
@@ -113,8 +115,8 @@ static MemoryLocation getStartLocation(Section const &section) {
}
// Returns a suitable free space index into `memory[section->type]` at which to place the given
// section, or -1 if none was found.
static ssize_t getPlacement(Section const &section, MemoryLocation &location) {
// section, or `std::nullopt` if none was found.
static std::optional<size_t> getPlacement(Section const &section, MemoryLocation &location) {
SectionTypeInfo const &typeInfo = sectionTypeInfo[section.type];
// Switch to the beginning of the next bank
@@ -168,7 +170,7 @@ static ssize_t getPlacement(Section const &section, MemoryLocation &location) {
// Try scrambled banks in descending order until no bank in the scrambled range is
// available. Otherwise, try in ascending order.
if (section.isBankFixed) {
return -1;
return std::nullopt;
} else if (options.scrambleROMX && section.type == SECTTYPE_ROMX
&& location.bank <= options.scrambleROMX) {
if (location.bank > typeInfo.firstBank) {
@@ -176,7 +178,7 @@ static ssize_t getPlacement(Section const &section, MemoryLocation &location) {
} else if (options.scrambleROMX < typeInfo.lastBank) {
location.bank = options.scrambleROMX + 1;
} else {
return -1;
return std::nullopt;
}
} else if (options.scrambleWRAMX && section.type == SECTTYPE_WRAMX
&& location.bank <= options.scrambleWRAMX) {
@@ -185,7 +187,7 @@ static ssize_t getPlacement(Section const &section, MemoryLocation &location) {
} else if (options.scrambleWRAMX < typeInfo.lastBank) {
location.bank = options.scrambleWRAMX + 1;
} else {
return -1;
return std::nullopt;
}
} else if (options.scrambleSRAM && section.type == SECTTYPE_SRAM
&& location.bank <= options.scrambleSRAM) {
@@ -194,12 +196,12 @@ static ssize_t getPlacement(Section const &section, MemoryLocation &location) {
} else if (options.scrambleSRAM < typeInfo.lastBank) {
location.bank = options.scrambleSRAM + 1;
} else {
return -1;
return std::nullopt;
}
} else if (location.bank < typeInfo.lastBank) {
++location.bank;
} else {
return -1;
return std::nullopt;
}
return getPlacement(section, location); // Tail recursion
@@ -209,7 +211,7 @@ static std::string getSectionDescription(Section const &section) {
std::string where;
char bank[8], addr[8], mask[8], offset[8];
if (section.isBankFixed && nbbanks(section.type) != 1) {
if (section.isBankFixed && sectTypeBanks(section.type) != 1) {
snprintf(bank, sizeof(bank), "%02" PRIx32, section.bank);
}
if (section.isAddressFixed) {
@@ -220,7 +222,7 @@ static std::string getSectionDescription(Section const &section) {
snprintf(offset, sizeof(offset), "%" PRIx16, section.alignOfs);
}
if (section.isBankFixed && nbbanks(section.type) != 1) {
if (section.isBankFixed && sectTypeBanks(section.type) != 1) {
if (section.isAddressFixed) {
where = "at $";
where += bank;
@@ -271,10 +273,10 @@ static void placeSection(Section &section) {
// Place section using first-fit decreasing algorithm
// https://en.wikipedia.org/wiki/Bin_packing_problem#First-fit_algorithm
MemoryLocation location = getStartLocation(section);
if (ssize_t spaceIdx = getPlacement(section, location); spaceIdx != -1) {
if (std::optional<size_t> spaceIdx = getPlacement(section, location); spaceIdx) {
std::deque<FreeSpace> &bankMem =
memory[section.type][location.bank - sectionTypeInfo[section.type].firstBank];
FreeSpace &freeSpace = bankMem[spaceIdx];
FreeSpace &freeSpace = bankMem[*spaceIdx];
assignSection(section, location);
@@ -284,17 +286,17 @@ static void placeSection(Section &section) {
bool noRightSpace = freeSpace.address + freeSpace.size == sectionEnd;
if (noLeftSpace && noRightSpace) {
// The free space is entirely deleted
bankMem.erase(bankMem.begin() + spaceIdx);
bankMem.erase(bankMem.begin() + *spaceIdx);
} else if (!noLeftSpace && !noRightSpace) {
// The free space is split in two
// Append the new space after the original one
uint16_t size = static_cast<uint16_t>(freeSpace.address + freeSpace.size - sectionEnd);
bankMem.insert(bankMem.begin() + spaceIdx + 1, {.address = sectionEnd, .size = size});
bankMem.insert(bankMem.begin() + *spaceIdx + 1, {.address = sectionEnd, .size = size});
// **`freeSpace` cannot be reused from this point on, because `bankMem.insert`
// invalidates all references to itself!**
// Resize the original space (address is unmodified)
bankMem[spaceIdx].size = section.org - bankMem[spaceIdx].address;
bankMem[*spaceIdx].size = section.org - bankMem[*spaceIdx].address;
} else {
// The amount of free spaces doesn't change: resize!
freeSpace.size -= section.size;
@@ -314,7 +316,7 @@ static void placeSection(Section &section) {
sectionTypeInfo[section.type].name.c_str(),
getSectionDescription(section).c_str()
);
} else if (section.org + section.size > endaddr(section.type) + 1) {
} else if (section.org + section.size > sectTypeEndAddr(section.type) + 1) {
// If the section just can't fit the bank, report that
fatal(
"Unable to place \"%s\" (%s section) %s: section runs past end of region ($%04x > "
@@ -323,7 +325,7 @@ static void placeSection(Section &section) {
sectionTypeInfo[section.type].name.c_str(),
getSectionDescription(section).c_str(),
section.org + section.size,
endaddr(section.type) + 1
sectTypeEndAddr(section.type) + 1
);
} else {
// Otherwise there is overlap with another section
@@ -405,7 +407,7 @@ static std::vector<Section const *> checkOverlayCompat() {
}
void assign_AssignSections() {
verbosePrint("Beginning assignment...\n");
verbosePrint(VERB_NOTICE, "Beginning assignment...\n");
// Initialize assignment
initFreeSpace();
@@ -443,7 +445,7 @@ void assign_AssignSections() {
// Assign sections in decreasing constraint order
for (uint8_t constraints = std::size(unassignedSections); constraints--;) {
if (char const *constraintName = constraintNames[constraints]; constraintName) {
verbosePrint("Assigning %sconstrained sections...\n", constraintName);
verbosePrint(VERB_INFO, "Assigning %sconstrained sections...\n", constraintName);
} else {
assume(unassignedSections[constraints].empty());
}

56
src/link/fstack.cpp Normal file
View File

@@ -0,0 +1,56 @@
#include "link/fstack.hpp"
#include <stdint.h>
#include <string>
#include <utility>
#include <variant>
#include <vector>
#include "backtrace.hpp"
#include "helpers.hpp"
#include "linkdefs.hpp"
#include "link/warning.hpp"
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 {}; // LCOV_EXCL_LINE
}
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 reptChain = traceNodes.back().first;
for (uint32_t iter : node.iters()) {
reptChain.append("::REPT~");
reptChain.append(std::to_string(iter));
}
traceNodes.emplace_back(reptChain, curLineNo);
} else {
traceNodes.emplace_back(node.name(), curLineNo);
}
return traceNodes;
}
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; }
);
}

View File

@@ -5,12 +5,12 @@
#include <array>
#include <bit>
#include <inttypes.h>
#include <stdint.h>
#include <vector>
#include "helpers.hpp"
#include "util.hpp"
#include "linkdefs.hpp"
#include "link/lexer.hpp" // lexer_Error
#include "link/section.hpp"
#include "link/warning.hpp"
@@ -31,7 +31,7 @@ static void setActiveTypeAndIdx(SectionType type, uint32_t idx) {
}
void layout_SetFloatingSectionType(SectionType type) {
if (nbbanks(type) == 1) {
if (sectTypeBanks(type) == 1) {
// There is only a single bank anyway, so just set the index to 0.
setActiveTypeAndIdx(type, 0);
} else {
@@ -46,8 +46,8 @@ void layout_SetFloatingSectionType(SectionType type) {
}
void layout_SetSectionType(SectionType type) {
if (nbbanks(type) != 1) {
lexer_Error("A bank number must be specified for %s", sectionTypeInfo[type].name.c_str());
if (sectTypeBanks(type) != 1) {
scriptError("A bank number must be specified for %s", sectionTypeInfo[type].name.c_str());
// Keep going with a default value for the bank index.
}
@@ -58,16 +58,16 @@ void layout_SetSectionType(SectionType type, uint32_t bank) {
SectionTypeInfo const &typeInfo = sectionTypeInfo[type];
if (bank < typeInfo.firstBank) {
lexer_Error(
"%s bank %" PRIu32 " doesn't exist (the minimum is %" PRIu32 ")",
scriptError(
"%s bank %" PRIu32 " does not exist (the minimum is %" PRIu32 ")",
typeInfo.name.c_str(),
bank,
typeInfo.firstBank
);
bank = typeInfo.firstBank;
} else if (bank > typeInfo.lastBank) {
lexer_Error(
"%s bank %" PRIu32 " doesn't exist (the maximum is %" PRIu32 ")",
scriptError(
"%s bank %" PRIu32 " does not exist (the maximum is %" PRIu32 ")",
typeInfo.name.c_str(),
bank,
typeInfo.lastBank
@@ -79,11 +79,11 @@ void layout_SetSectionType(SectionType type, uint32_t bank) {
void layout_SetAddr(uint32_t addr) {
if (activeType == SECTTYPE_INVALID) {
lexer_Error("Cannot set the current address: no memory region is active");
scriptError("Cannot set the current address: no memory region is active");
return;
}
if (activeBankIdx == UINT32_MAX) {
lexer_Error("Cannot set the current address: the bank is floating");
scriptError("Cannot set the current address: the bank is floating");
return;
}
@@ -91,15 +91,15 @@ void layout_SetAddr(uint32_t addr) {
SectionTypeInfo const &typeInfo = sectionTypeInfo[activeType];
if (addr < pc) {
lexer_Error("Cannot decrease the current address (from $%04x to $%04x)", pc, addr);
} else if (addr > endaddr(activeType)) { // Allow "one past the end" sections.
lexer_Error(
scriptError("Cannot decrease the current address (from $%04x to $%04x)", pc, addr);
} else if (addr > sectTypeEndAddr(activeType)) { // Allow "one past the end" sections.
scriptError(
"Cannot set the current address to $%04" PRIx32 ": %s ends at $%04" PRIx16,
addr,
typeInfo.name.c_str(),
endaddr(activeType)
sectTypeEndAddr(activeType)
);
pc = endaddr(activeType);
pc = sectTypeEndAddr(activeType);
} else {
pc = addr;
}
@@ -108,7 +108,7 @@ void layout_SetAddr(uint32_t addr) {
void layout_MakeAddrFloating() {
if (activeType == SECTTYPE_INVALID) {
lexer_Error("Cannot make the current address floating: no memory region is active");
scriptError("Cannot make the current address floating: no memory region is active");
return;
}
@@ -119,7 +119,7 @@ void layout_MakeAddrFloating() {
void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
if (activeType == SECTTYPE_INVALID) {
lexer_Error("Cannot align: no memory region is active");
scriptError("Cannot align: no memory region is active");
return;
}
@@ -130,7 +130,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
uint32_t alignSize = 1u << alignment;
if (alignOfs >= alignSize) {
lexer_Error(
scriptError(
"Cannot align: The alignment offset (%" PRIu32
") must be less than alignment size (%" PRIu32 ")",
alignOfs,
@@ -149,7 +149,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
uint16_t &pc = curAddr[activeType][activeBankIdx];
if (alignment > 16) {
lexer_Error("Cannot align: The alignment (%" PRIu32 ") must be less than 16", alignment);
scriptError("Cannot align: The alignment (%" PRIu32 ") must be less than 16", alignment);
return;
}
@@ -160,7 +160,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
uint32_t alignSize = 1u << alignment;
if (alignOfs >= alignSize) {
lexer_Error(
scriptError(
"Cannot align: The alignment offset (%" PRIu32
") must be less than alignment size (%" PRIu32 ")",
alignOfs,
@@ -174,12 +174,12 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
}
if (uint16_t offset = pc - typeInfo.startAddr; length > typeInfo.size - offset) {
lexer_Error(
scriptError(
"Cannot align: the next suitable address after $%04" PRIx16 " is $%04" PRIx16
", past $%04" PRIx16,
pc,
static_cast<uint16_t>(pc + length),
static_cast<uint16_t>(endaddr(activeType) + 1)
static_cast<uint16_t>(sectTypeEndAddr(activeType) + 1)
);
return;
}
@@ -189,7 +189,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
void layout_Pad(uint32_t length) {
if (activeType == SECTTYPE_INVALID) {
lexer_Error("Cannot increase the current address: no memory region is active");
scriptError("Cannot increase the current address: no memory region is active");
return;
}
@@ -203,11 +203,11 @@ void layout_Pad(uint32_t length) {
assume(pc >= typeInfo.startAddr);
if (uint16_t offset = pc - typeInfo.startAddr; length + offset > typeInfo.size) {
lexer_Error(
scriptError(
"Cannot increase the current address by %u bytes: only %u bytes to $%04" PRIx16,
length,
typeInfo.size - offset,
static_cast<uint16_t>(endaddr(activeType) + 1)
static_cast<uint16_t>(sectTypeEndAddr(activeType) + 1)
);
} else {
pc += length;
@@ -216,14 +216,14 @@ void layout_Pad(uint32_t length) {
void layout_PlaceSection(std::string const &name, bool isOptional) {
if (activeType == SECTTYPE_INVALID) {
lexer_Error("No memory region has been specified to place section \"%s\" in", name.c_str());
scriptError("No memory region has been specified to place section \"%s\" in", name.c_str());
return;
}
Section *section = sect_GetSection(name.c_str());
if (!section) {
if (!isOptional) {
lexer_Error("Unknown section \"%s\"", name.c_str());
scriptError("Undefined section \"%s\"", name.c_str());
}
return;
}
@@ -233,17 +233,17 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
// Check that the linker script doesn't contradict what the code says.
if (section->type == SECTTYPE_INVALID) {
// A section that has data must get assigned a type that requires data.
if (!sect_HasData(activeType) && !section->data.empty()) {
lexer_Error(
if (!sectTypeHasData(activeType) && !section->data.empty()) {
scriptError(
"\"%s\" is specified to be a %s section, but it contains data",
name.c_str(),
typeInfo.name.c_str()
);
} else if (sect_HasData(activeType) && section->data.empty() && section->size != 0) {
} else if (sectTypeHasData(activeType) && section->data.empty() && section->size != 0) {
// A section that lacks data can only be assigned to a type that requires data
// if it's empty.
lexer_Error(
"\"%s\" is specified to be a %s section, but it doesn't contain data",
scriptError(
"\"%s\" is specified to be a %s section, but it does not contain data",
name.c_str(),
typeInfo.name.c_str()
);
@@ -255,7 +255,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
}
}
} else if (section->type != activeType) {
lexer_Error(
scriptError(
"\"%s\" is specified to be a %s section, but it is already a %s section",
name.c_str(),
typeInfo.name.c_str(),
@@ -268,7 +268,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
} else {
uint32_t bank = activeBankIdx + typeInfo.firstBank;
if (section->isBankFixed && bank != section->bank) {
lexer_Error(
scriptError(
"The linker script places section \"%s\" in %s bank %" PRIu32
", but it was already defined in bank %" PRIu32,
name.c_str(),
@@ -284,7 +284,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
if (!isPcFloating) {
uint16_t &org = curAddr[activeType][activeBankIdx];
if (section->isAddressFixed && org != section->org) {
lexer_Error(
scriptError(
"The linker script assigns section \"%s\" to address $%04" PRIx16
", but it was already at $%04" PRIx16,
name.c_str(),
@@ -293,7 +293,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
);
} else if (section->isAlignFixed && (org & section->alignMask) != section->alignOfs) {
uint8_t alignment = std::countr_one(section->alignMask);
lexer_Error(
scriptError(
"The linker script assigns section \"%s\" to address $%04" PRIx16
", but that would be ALIGN[%" PRIu8 ", %" PRIu16
"] instead of the requested ALIGN[%" PRIu8 ", %" PRIu16 "]",
@@ -312,7 +312,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
uint16_t curOfs = org - typeInfo.startAddr;
if (section->size > typeInfo.size - curOfs) {
uint16_t overflowSize = section->size - (typeInfo.size - curOfs);
lexer_Error(
scriptError(
"The linker script assigns section \"%s\" to address $%04" PRIx16
", but then it would overflow %s by %" PRIu16 " byte%s",
name.c_str(),

View File

@@ -2,15 +2,19 @@
#include "link/lexer.hpp"
#include <array>
#include <errno.h>
#include <fstream>
#include <inttypes.h>
#include <ios>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <utility>
#include <vector>
#include "backtrace.hpp"
#include "helpers.hpp"
#include "itertools.hpp"
#include "linkdefs.hpp"
#include "util.hpp"
#include "link/warning.hpp"
@@ -27,12 +31,12 @@ struct LexerStackEntry {
static std::vector<LexerStackEntry> lexerStack;
void lexer_Error(char const *fmt, ...) {
LexerStackEntry &context = lexerStack.back();
va_list args;
va_start(args, fmt);
scriptError(context.path.c_str(), context.lineNo, fmt, args);
va_end(args);
void lexer_TraceCurrent() {
trace_PrintBacktrace(
lexerStack,
[](LexerStackEntry const &context) { return context.path.c_str(); },
[](LexerStackEntry const &context) { return context.lineNo; }
);
}
void lexer_IncludeFile(std::string &&path) {
@@ -46,7 +50,7 @@ void lexer_IncludeFile(std::string &&path) {
std::string badPath = std::move(newContext.path);
lexerStack.pop_back();
// This error will occur in `prevContext`, *before* incrementing the line number!
lexer_Error(
scriptError(
"Failed to open included linker script \"%s\": %s", badPath.c_str(), strerror(errno)
);
}
@@ -60,14 +64,6 @@ void lexer_IncLineNo() {
++lexerStack.back().lineNo;
}
static bool isWhiteSpace(int c) {
return c == ' ' || c == '\t';
}
static bool isNewline(int c) {
return c == '\r' || c == '\n';
}
yy::parser::symbol_type yylex(); // Forward declaration for `yywrap`
static yy::parser::symbol_type yywrap() {
@@ -89,28 +85,20 @@ static yy::parser::symbol_type yywrap() {
return yy::parser::make_YYEOF();
}
static bool isIdentChar(int c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
}
static std::string readIdent(int c) {
static std::string readKeyword(int c) {
LexerStackEntry &context = lexerStack.back();
std::string ident;
ident.push_back(c);
for (c = context.file.sgetc(); isIdentChar(c); c = context.file.snextc()) {
ident.push_back(c);
std::string keyword;
keyword.push_back(c);
for (c = context.file.sgetc(); isAlphanumeric(c); c = context.file.snextc()) {
keyword.push_back(c);
}
return ident;
}
static bool isDecDigit(int c) {
return c >= '0' && c <= '9';
return keyword;
}
static yy::parser::symbol_type parseDecNumber(int c) {
LexerStackEntry &context = lexerStack.back();
uint32_t number = c - '0';
for (c = context.file.sgetc(); isDecDigit(c) || c == '_'; c = context.file.sgetc()) {
for (c = context.file.sgetc(); isDigit(c) || c == '_'; c = context.file.sgetc()) {
if (c != '_') {
number = number * 10 + (c - '0');
}
@@ -127,7 +115,7 @@ static yy::parser::symbol_type parseBinNumber(char const *prefix) {
LexerStackEntry &context = lexerStack.back();
int c = context.file.sgetc();
if (!isBinDigit(c)) {
lexer_Error("No binary digits found after '%s'", prefix);
scriptError("No binary digits found after %s", prefix);
return yy::parser::make_number(0);
}
@@ -142,15 +130,11 @@ static yy::parser::symbol_type parseBinNumber(char const *prefix) {
return yy::parser::make_number(number);
}
static bool isOctDigit(int c) {
return c >= '0' && c <= '7';
}
static yy::parser::symbol_type parseOctNumber(char const *prefix) {
LexerStackEntry &context = lexerStack.back();
int c = context.file.sgetc();
if (!isOctDigit(c)) {
lexer_Error("No octal digits found after '%s'", prefix);
scriptError("No octal digits found after %s", prefix);
return yy::parser::make_number(0);
}
@@ -165,12 +149,8 @@ static yy::parser::symbol_type parseOctNumber(char const *prefix) {
return yy::parser::make_number(number);
}
static bool isHexDigit(int c) {
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
static uint8_t parseHexDigit(int c) {
if (c >= '0' && c <= '9') {
if (isDigit(c)) {
return c - '0';
} else if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
@@ -185,7 +165,7 @@ static yy::parser::symbol_type parseHexNumber(char const *prefix) {
LexerStackEntry &context = lexerStack.back();
int c = context.file.sgetc();
if (!isHexDigit(c)) {
lexer_Error("No hexadecimal digits found after '%s'", prefix);
scriptError("No hexadecimal digits found after %s", prefix);
return yy::parser::make_number(0);
}
@@ -206,22 +186,22 @@ static yy::parser::symbol_type parseNumber(int c) {
switch (context.file.sgetc()) {
case 'x':
context.file.sbumpc();
return parseHexNumber("0x");
return parseHexNumber("\"0x\"");
case 'X':
context.file.sbumpc();
return parseHexNumber("0X");
return parseHexNumber("\"0X\"");
case 'o':
context.file.sbumpc();
return parseOctNumber("0o");
return parseOctNumber("\"0o\"");
case 'O':
context.file.sbumpc();
return parseOctNumber("0O");
return parseOctNumber("\"0O\"");
case 'b':
context.file.sbumpc();
return parseBinNumber("0b");
return parseBinNumber("\"0b\"");
case 'B':
context.file.sbumpc();
return parseBinNumber("0B");
return parseBinNumber("\"0B");
}
}
return parseDecNumber(c);
@@ -233,14 +213,14 @@ static yy::parser::symbol_type parseString() {
std::string str;
for (; c != '"'; c = context.file.sgetc()) {
if (c == EOF || isNewline(c)) {
lexer_Error("Unterminated string");
scriptError("Unterminated string");
break;
}
context.file.sbumpc();
if (c == '\\') {
c = context.file.sgetc();
if (c == EOF || isNewline(c)) {
lexer_Error("Unterminated string");
scriptError("Unterminated string");
break;
} else if (c == 'n') {
c = '\n';
@@ -251,7 +231,7 @@ static yy::parser::symbol_type parseString() {
} else if (c == '0') {
c = '\0';
} else if (c != '\\' && c != '"' && c != '\'') {
lexer_Error("Cannot escape character %s", printChar(c));
scriptError("Cannot escape character %s", printChar(c));
}
context.file.sbumpc();
}
@@ -267,8 +247,8 @@ yy::parser::symbol_type yylex() {
LexerStackEntry &context = lexerStack.back();
int c = context.file.sbumpc();
// First, skip leading whitespace.
while (isWhiteSpace(c)) {
// First, skip leading blank space.
while (isBlankSpace(c)) {
c = context.file.sbumpc();
}
// Then, skip a comment if applicable.
@@ -292,15 +272,15 @@ yy::parser::symbol_type yylex() {
} else if (c == '"') {
return parseString();
} else if (c == '$') {
return parseHexNumber("$");
return parseHexNumber("'$'");
} else if (c == '%') {
return parseBinNumber("%");
return parseBinNumber("'%'");
} else if (c == '&') {
return parseOctNumber("&");
} else if (isDecDigit(c)) {
return parseOctNumber("'&'");
} else if (isDigit(c)) {
return parseNumber(c);
} else if (isIdentChar(c)) { // Note that we match these *after* digit characters!
std::string ident = readIdent(c);
} else if (isLetter(c)) {
std::string keyword = readKeyword(c);
static UpperMap<SectionType> const sectTypes{
{"WRAM0", SECTTYPE_WRAM0},
@@ -312,7 +292,7 @@ yy::parser::symbol_type yylex() {
{"SRAM", SECTTYPE_SRAM },
{"OAM", SECTTYPE_OAM },
};
if (auto search = sectTypes.find(ident); search != sectTypes.end()) {
if (auto search = sectTypes.find(keyword); search != sectTypes.end()) {
return yy::parser::make_sect_type(search->second);
}
@@ -324,14 +304,14 @@ yy::parser::symbol_type yylex() {
{"DS", yy::parser::make_DS },
{"OPTIONAL", yy::parser::make_OPTIONAL},
};
if (auto search = keywords.find(ident); search != keywords.end()) {
if (auto search = keywords.find(keyword); search != keywords.end()) {
return search->second();
}
lexer_Error("Unknown keyword \"%s\"", ident.c_str());
scriptError("Unknown keyword `%s`", keyword.c_str());
return yylex();
} else {
lexer_Error("Unexpected character %s", printChar(c));
scriptError("Unexpected character %s", printChar(c));
// Keep reading characters until the EOL, to avoid reporting too many errors.
for (c = context.file.sgetc(); !isNewline(c); c = context.file.sgetc()) {
if (c == EOF) {

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