Compare commits

..

52 Commits

Author SHA1 Message Date
Rangi
268b586c9d Release v1.0.0-rc2 2025-09-30 18:56:00 -04:00
Rangi
85d3b5df58 Add more RGBFIX tests 2025-09-30 18:20:53 -04:00
Rangi
eea277ae9c Add more tests for RGBFIX 2025-09-29 22:43:16 -04:00
Rangi
538395b92c Prevent simple syntax highlighters from breaking on /* in a string 2025-09-29 11:11:40 -04:00
Rangi
3108fb5297 Rename gbz80.7 "CPU opcode reference" to "Game Boy CPU instruction reference" 2025-09-28 16:43:36 -04:00
Rangi42
f99591bf6f Link FORCE_COLOR and NO_COLOR 2025-09-25 14:06:44 -04:00
Rangi42
0297da4d4c Add more tests for RGBASM coverage 2025-09-25 13:30:30 -04:00
Rangi42
96b953fe51 Add a test case for overlapping IF/ENDC and REPT/ENDR
This trips an asserton in the Rust rewrite because of its different
conditional stack design
2025-09-25 11:57:56 -04:00
Rangi42
0670c03bc2 Add CLI tests for RGBASM 2025-09-25 11:57:56 -04:00
Rangi42
09ef5b7e06 Refactor error fix suggestions in fstk_RunMacro 2025-09-24 19:35:41 -04:00
Rangi42
d5bb462f25 Separate isLetter into isUpper and isLower 2025-09-24 19:19:50 -04:00
Rangi42
b0727e9779 Suggest removing space before colon to define a label instead of invoking a macro 2025-09-24 18:32:45 -04:00
Rangi42
ca4b890273 Consistently do & alignMask, not % alignSize
Also add more unrelated tests for coverage
2025-09-23 13:25:51 -04:00
Rangi42
96a75500d3 Test gb-starter-kit from codeberg.org, not github.com 2025-09-23 13:06:12 -04:00
Rangi
634fd853d1 Factor out a single parseNumber utility function (#1839) 2025-09-22 15:15:24 -04:00
Rangi42
c8d22d8744 Refactor some iterator template code in RGBGFX pal_packing.cpp 2025-09-21 10:59:59 -04:00
Rangi42
3ece61b103 Use fewer templates in RGBGFX pal_packing.cpp 2025-09-20 22:00:17 -04:00
Rangi42
a82fd17529 Simplify RGBGFX code by using fewer templates 2025-09-20 21:06:51 -04:00
Rangi42
e7f5ab3f55 Warn about rgbgfx -O without -o or any of -A -T -P -Q 2025-09-20 20:23:29 -04:00
Rangi42
595c87b2f8 Update test dependencies 2025-09-20 15:23:02 -04:00
Rangi
c49a7d1e2f Make CLI and OPT options -p and -Q more consistent (#1834) 2025-09-20 13:54:28 -04:00
Rangi42
d8aff148bb Factor out RRANGE macro like RANGE 2025-09-19 16:53:44 -04:00
Rangi
e31bcabbaa Implement === and !== string comparison operators (#1832) 2025-09-19 14:06:36 -04:00
Rangi
67741ab428 Trim left space from macro args, even past block comments (#1831) 2025-09-19 13:44:18 -04:00
Rangi
e0a6199f83 Allow charmap to map 'characters' as well as "strings" (#1830) 2025-09-16 12:51:07 +02:00
Rangi42
6cffd991f7 Document rgbasm -Wempty-data-directive
Fixes #1829
2025-09-15 09:44:50 -04:00
Rangi42
1fdeb34e50 Fix too-short .out.bin expected test results
RGBASM tests now use `rgblink -x` instead of `dd` to get trimmed
test output. RGBLINK tests cannot do so because some of them
(e.g. bank-numbers.asm and sizeof-startof.asm) rely on ROMX
sections above 1, and `-x` implies `-t` which breaks that.
2025-09-15 09:25:33 -04:00
Rangi42
9f16881d64 Use for-each loop 2025-09-10 21:18:33 -04:00
Rangi
223b3d1921 More accurate 8-bit <=> 5-bit RGB color conversion (#1827) 2025-09-08 15:13:25 -04:00
Rangi
65d408eb5d Document more about the RGBDS architecture (#1809) 2025-09-07 15:34:52 -04:00
ReiquelApplegate
3677ab2ebf Correct -q's arg name in rgbgfx(1) (#1826) 2025-09-07 18:28:37 +02:00
Rangi42
d404621e0d Don't silence strings-as-numbers with OPT Wno-obsolete in tests 2025-09-07 12:17:39 -04:00
Rangi42
c8a088f281 Avoid nonessential EQUS expansion in tests 2025-09-06 13:45:12 -04:00
Rangi42
94ed28acf8 Avoid nonessential strings-as-numbers in tests 2025-09-06 12:45:01 -04:00
Rangi42
00b5077b2a Make usage info output responsive to the terminal width 2025-09-05 21:58:17 -04:00
Rangi42
024b33b63a make format should apply to all .cpp and .hpp files 2025-09-05 21:05:58 -04:00
Rangi42
dcc10cebc3 Add test for duplicate section after use of fragment literal
Verifies that #1822 was fixed (by c6997fe73c)
2025-09-05 20:14:01 -04:00
Rangi42
1fc9ba86c4 Some RGBLINK refactoring
- Consistently refer to `Section` fragments/unions as "pieces" (renaming `.nextu`)
- Remove `Symbol`'s `.label()` accessors (use `std::get<Label>`)
- Move some `Label`-related logic into `Symbol` methods
2025-09-05 16:34:51 -04:00
Rangi42
e569e0c200 Don't comment "; Next fragment/union" in .map files for empty section pieces
Fixes #1821
2025-09-05 15:31:46 -04:00
Rangi42
f7fb3af615 Run make tidy with Checks: '-*,misc-include-cleaner' in .clang-tidy (IWYU) 2025-09-04 13:39:23 -04:00
Rangi42
1dfc1d3231 Factor out isBinDigit and parseHexDigit utility functions 2025-09-04 13:23:10 -04:00
Rangi42
891e6f98df Fix formatting of very long fixed-point numbers 2025-09-04 12:54:14 -04:00
Rangi42
bdc885bd69 Avoid UB when checking truncation range
Fixes #1818
2025-09-04 12:04:10 -04:00
Rangi42
5b67dc94b6 Add more test coverage 2025-09-04 01:29:50 -04:00
Rangi42
4f702a4be8 Try to optimize RPN expressions with .emplace_back instead of .push_back 2025-09-03 23:00:06 -04:00
Rangi42
c5d437ab3c Tell people to use character literals or CHARVAL instead of strings as numbers 2025-09-03 22:46:19 -04:00
Rangi42
c5c2800f17 Move RPN buffer encoding logic into rpn.cpp 2025-09-03 22:36:00 -04:00
Rangi42
c798500563 Don't call rpn.clear() when we can safely assume it's already empty() 2025-09-03 21:02:57 -04:00
Rangi
590d113e94 Use a vector of RPN values (#1820)
This is instead of byte-encoding them in a different way than the actual object output's RPN buffer
2025-09-03 14:42:37 -04:00
Rangi
ee1db0a582 Fix RPN patches for all commands (#1819) 2025-09-02 16:44:25 -04:00
Rangi42
5701d747d4 Document the stricter rules for lexing underscores in integer constants 2025-09-02 14:39:28 -04:00
Rangi42
2110aaca20 Fix RGBLINK and RGBGFX warning/error message colors 2025-09-01 20:51:33 -04:00
284 changed files with 2299 additions and 1435 deletions

View File

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

View File

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

View File

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

View File

@@ -44,5 +44,5 @@ RGBGFX generates palettes using algorithms found in the paper
([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).
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=1.0.0-rc1
ARG version=1.0.0-rc2
WORKDIR /rgbds
COPY . .

View File

@@ -241,12 +241,12 @@ coverage:
# Target used in development to format source code with clang-format.
format:
$Qclang-format -i $$(git ls-files 'include/**/*.hpp' 'src/**/*.cpp')
$Qclang-format -i $$(git ls-files '*.hpp' '*.cpp')
# Target used in development to check code with clang-tidy.
# Requires Bison-generated header files to exist.
tidy: src/asm/parser.hpp src/link/script.hpp
$Qclang-tidy -p . $$(git ls-files 'include/**/*.hpp' 'src/**/*.cpp')
$Qclang-tidy -p . $$(git ls-files '*.hpp' '*.cpp')
# Target used in development to remove unused `#include` headers.
iwyu:

View File

@@ -12,10 +12,12 @@
struct Expression;
struct FileStackNode;
struct Symbol;
enum StateFeature { STATE_EQU, STATE_VAR, STATE_EQUS, STATE_CHAR, STATE_MACRO, NB_STATE_FEATURES };
void out_RegisterNode(std::shared_ptr<FileStackNode> node);
void out_RegisterSymbol(Symbol &sym);
void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32_t pcShift);
void out_CreateAssert(
AssertionType type, Expression const &expr, std::string const &message, uint32_t ofs

View File

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

View File

@@ -41,7 +41,6 @@ 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

View File

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

View File

@@ -97,6 +97,7 @@ static inline int clz(unsigned int x) {
// For lack of <ranges>, this adds some more brevity
#define RANGE(s) std::begin(s), std::end(s)
#define RRANGE(s) std::rbegin(s), std::rend(s)
// MSVC does not inline `strlen()` or `.length()` of a constant string
template<int N>

View File

@@ -8,6 +8,7 @@
#include <stddef.h>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <utility>

View File

@@ -48,7 +48,7 @@ struct Section {
// Extra info computed during linking
std::vector<Symbol> *fileSymbols;
std::vector<Symbol *> symbols;
std::unique_ptr<Section> nextu; // The next "component" of this unionized sect
std::unique_ptr<Section> nextPiece; // The next fragment or union "piece" of this section
};
// Execute a callback for each section currently registered.

View File

@@ -33,8 +33,8 @@ struct Symbol {
>
data;
Label &label() { return std::get<Label>(data); }
Label const &label() const { return std::get<Label>(data); }
void linkToSection(Section &section);
void fixSectionOffset();
};
void sym_ForEach(void (*callback)(Symbol &));

View File

@@ -55,7 +55,7 @@
#define setmode(fd, mode) (0)
#endif
// MingGW and Cygwin need POSIX functions which are not standard C explicitly enabled
// MingGW and Cygwin need POSIX functions which are not standard C explicitly enabled,
#if defined(__MINGW32__) || defined(__CYGWIN__)
#define _POSIX_C_SOURCE 200809L
#endif

View File

@@ -4,20 +4,33 @@
#define RGBDS_UTIL_HPP
#include <algorithm>
#include <ctype.h>
#include <ctype.h> // toupper
#include <numeric>
#include <optional>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <unordered_map>
#include "helpers.hpp"
enum NumberBase {
BASE_AUTO = 0,
BASE_2 = 2,
BASE_8 = 8,
BASE_10 = 10,
BASE_16 = 16,
};
bool isNewline(int c);
bool isBlankSpace(int c);
bool isWhitespace(int c);
bool isPrintable(int c);
bool isUpper(int c);
bool isLower(int c);
bool isLetter(int c);
bool isDigit(int c);
bool isBinDigit(int c);
bool isOctDigit(int c);
bool isHexDigit(int c);
bool isAlphanumeric(int c);
@@ -25,6 +38,10 @@ bool isAlphanumeric(int c);
bool startsIdentifier(int c);
bool continuesIdentifier(int c);
uint8_t parseHexDigit(int c);
std::optional<uint64_t> parseNumber(char const *&str, NumberBase base = BASE_AUTO);
std::optional<uint64_t> parseWholeNumber(char const *str, NumberBase base = BASE_AUTO);
char const *printChar(int c);
struct Uppercase {

View File

@@ -3,6 +3,8 @@
#ifndef RGBDS_VERBOSITY_HPP
#define RGBDS_VERBOSITY_HPP
#include <stdio.h>
#include "style.hpp"
// This macro does not evaluate its arguments unless the condition is true.

View File

@@ -6,7 +6,7 @@
#define PACKAGE_VERSION_MAJOR 1
#define PACKAGE_VERSION_MINOR 0
#define PACKAGE_VERSION_PATCH 0
#define PACKAGE_VERSION_RC 1
#define PACKAGE_VERSION_RC 2
char const *get_package_version_string();

View File

@@ -1,13 +1,13 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd September 1, 2025
.Dd September 30, 2025
.Dt GBZ80 7
.Os
.Sh NAME
.Nm gbz80
.Nd CPU opcode reference
.Nd Game Boy CPU instruction reference
.Sh DESCRIPTION
This is the list of opcodes supported by
This is the list of instructions supported by
.Xr rgbasm 1 ,
including a short description, the number of bytes needed to encode them and the number of CPU cycles at 1MHz (or 2MHz in GBC double speed mode) needed to complete them.
.Pp

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd September 1, 2025
.Dd September 30, 2025
.Dt RGBASM-OLD 5
.Os
.Sh NAME
@@ -371,7 +371,7 @@ Deprecated in 1.0.0.
.Pp
Instead, use
.Dl -Wno-overwrite .
.Ss rgbgfx -h
.Ss rgbgfx -h/--horizontal
Removed in 0.6.0.
.Pp
Instead, use
@@ -461,6 +461,24 @@ Previously we had
.Pp
Instead, now we have
.Ql p ** q ** r == p ** (q ** r) .
.Ss 8-bit and 5-bit color conversion
Changed in 1.0.0.
.Pp
RGBGFX takes 8-bit RGB colors as its PNG input, and outputs 5-bit GBC colors.
Its
.Ql -r/--reverse
mode does the opposite 5-bit to 8-bit conversion.
Instead of the previous inaccurate conversions, we now do accurate rounding to the nearest equivalent.
.Pp
Previously to convert an 8-bit color channel to 5-bit, we truncated it as
.Ql c >> 3 ;
and to reverse a 5-bit color channel to 8-bit, we extended it as
.Ql (c << 3) | (c >> 2) .
.Pp
Instead, now we round 8-bit to 5-bit as
.Ql (c * 31 + 127) / 255 ,
and round 5-bit to 8-bit as
.Ql (c * 255 + 15) / 31 .
.Sh BUGS
These are misfeatures that may have been possible by mistake.
They do not get deprecated, just fixed.
@@ -491,6 +509,13 @@ Instead, use
.Ql Label:
and
.Ql Label:: .
.Ss Extra underscores in integer constants
Fixed in 1.0.0.
.Pp
Underscores, the optional digit separators in integer constants, used to allow more than one in sequence, or trailing without digits on either side.
Now only one underscore is allowed between two digits, or between the base prefix and a digit, or between a digit and the
.Ql q
fixed-point precision suffix.
.Ss ADD r16 with implicit first HL operand
Fixed in 0.5.0.
.Pp

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd September 1, 2025
.Dd September 30, 2025
.Dt RGBASM 1
.Os
.Sh NAME
@@ -384,6 +384,15 @@ This warning is enabled by
.Fl Wall .
.It Fl Wdiv
Warn when dividing the smallest negative integer (-2**31) by -1, which yields itself due to integer overflow.
.It Fl Wempty-data-directive
Warn when
.Ic DB ,
.Ic DW ,
or
.Ic DL
is used without an argument in a ROM section.
This warning is enabled by
.Fl Wall .
.It Fl Wempty-macro-arg
Warn when a macro argument is empty.
This warning is enabled by

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd September 1, 2025
.Dd September 30, 2025
.Dt RGBASM 5
.Os
.Sh NAME
@@ -586,6 +586,19 @@ is equivalent to
or to
.Ql STRCAT("str", \&"ing") .
.Pp
You can use the
.Sq ===
and
.Sq !==
operators to compare two strings.
.Ql \&"str" === \&"ing"
is equivalent to
.Ql STRCMP("str", \&"ing") == 0 ,
and
.Ql \&"str" !== \&"ing"
is equivalent to
.Ql STRCMP("str", \&"ing") != 0 .
.Pp
The following functions operate on string expressions, and return strings themselves.
.Bl -column "STRSLICE(str, start, stop)"
.It Sy Name Ta Sy Operation
@@ -638,12 +651,12 @@ and the values of a charmap entry are counted by
When writing text strings that are meant to be displayed on the Game Boy, the character encoding in the ROM may need to be different than the source file encoding.
For example, the tiles used for uppercase letters may be placed starting at tile index 128, which differs from ASCII starting at 65.
.Pp
Character maps allow mapping strings to arbitrary sequences of numbers:
Character maps allow mapping strings or character literals to arbitrary sequences of numbers:
.Bd -literal -offset indent
CHARMAP "A", 42
CHARMAP ":)", 39
CHARMAP ':)', 39
CHARMAP "<br>", 13, 10
CHARMAP "&euro;", $20ac
CHARMAP '&euro;', $20ac
.Ed
.Pp
This would result in

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd September 1, 2025
.Dd September 30, 2025
.Dt RGBDS 5
.Os
.Sh NAME
@@ -405,9 +405,6 @@ Checks if the value is a valid bit index
.Pq see e.g. Do BIT u3, r8 Dc in Xr gbz80 7 ,
that is, from 0 to 7.
The value is then ORed with the instruction's mask.
.It Li $80 Ta Integer literal; followed by the
.Cm LONG
integer.
.It Li $70 Ta Cm HIGH
byte.
.It Li $71 Ta Cm LOW
@@ -416,6 +413,9 @@ byte.
value.
.It Li $73 Ta Cm TZCOUNT
value.
.It Li $80 Ta Integer literal; followed by the
.Cm LONG
integer.
.It Li $81 Ta A symbol's value; followed by the symbol's
.Cm LONG
ID.

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd September 1, 2025
.Dd September 30, 2025
.Dt RGBGFX 1
.Os
.Sh NAME
@@ -347,7 +347,7 @@ Output the image's palette set to this file.
Same as
.Fl p Ar base_path Ns .pal
.Pq see Sx Automatic output paths .
.It Fl q Ar pal_file , Fl \-palette-map Ar pal_file
.It Fl q Ar pal_map , Fl \-palette-map Ar pal_map
Output the image's palette map to this file.
This is useful if the input image contains more than 8 palettes, as the attribute map only contains the lower 3 bits of the palette indices.
.It Fl Q , Fl \-auto-palette-map

View File

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

View File

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

View File

@@ -47,7 +47,9 @@ void act_Elif(int32_t condition) {
if (lexer_ReachedELSEBlock()) {
fatal("Found `ELIF` after an `ELSE` block");
}
lexer_SetMode(LEXER_SKIP_TO_ENDC);
// This should be redundant, as the lexer will have skipped to `ENDC` since
// an `ELIF` after a taken `IF` needs to not evaluate its condition.
lexer_SetMode(LEXER_SKIP_TO_ENDC); // LCOV_EXCL_LINE
} else if (condition) {
lexer_RunIFBlock();
} else {
@@ -61,7 +63,8 @@ void act_Else() {
}
if (lexer_RanIFBlock()) {
if (lexer_ReachedELSEBlock()) {
fatal("Found `ELSE` after an `ELSE` block");
// This should be redundant, as the lexer handles this error first.
fatal("Found `ELSE` after an `ELSE` block"); // LCOV_EXCL_LINE
}
lexer_SetMode(LEXER_SKIP_TO_ENDC);
} else {
@@ -186,7 +189,10 @@ uint32_t act_CharToNum(std::string const &str) {
}
uint32_t act_StringToNum(std::string const &str) {
warning(WARNING_OBSOLETE, "Treating strings as numbers is deprecated");
warning(
WARNING_OBSOLETE,
"Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead"
);
if (std::vector<int32_t> units = charmap_Convert(str); units.size() == 1) {
// The string is a single character with a single unit value,
// which can be used directly as a number.

View File

@@ -11,68 +11,57 @@
#include <string.h>
#include <string>
#include "util.hpp" // isDigit
#include "util.hpp" // parseNumber
#include "asm/main.hpp" // options
#include "asm/warning.hpp"
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');
}
return i;
}
size_t FormatSpec::parseSpec(char const *spec) {
size_t i = 0;
auto parseSpecNumber = [&spec, &i]() {
char const *end = &spec[i];
size_t number = parseNumber(end, BASE_10).value_or(0);
i += end - &spec[i];
return number;
};
// <sign>
if (char c = spec[i]; c == ' ' || c == '+') {
++i;
sign = c;
}
// <exact>
if (spec[i] == '#') {
++i;
exact = true;
}
// <align>
if (spec[i] == '-') {
++i;
alignLeft = true;
}
// <pad>
if (spec[i] == '0') {
++i;
padZero = true;
}
// <width>
if (isDigit(spec[i])) {
i += parseNumber(&spec[i], width);
width = parseSpecNumber();
}
// <frac>
if (spec[i] == '.') {
++i;
hasFrac = true;
i += parseNumber(&spec[i], fracWidth);
fracWidth = parseSpecNumber();
}
// <prec>
if (spec[i] == 'q') {
++i;
hasPrec = true;
i += parseNumber(&spec[i], precision);
precision = parseSpecNumber();
}
// <type>
switch (char c = spec[i]; c) {
case 'd':
@@ -87,7 +76,7 @@ size_t FormatSpec::parseSpec(char const *spec) {
type = c;
break;
}
// Done parsing
parsed = true;
return i;
}
@@ -188,36 +177,32 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
if (useType == 'd' || useType == 'f') {
if (int32_t v = value; v < 0) {
signChar = '-';
if (v != INT32_MIN) {
if (v != INT32_MIN) { // -INT32_MIN is UB
value = -v;
}
}
}
char prefixChar = !useExact ? 0
: useType == 'X' ? '$'
: useType == 'x' ? '$'
: useType == 'b' ? '%'
: useType == 'o' ? '&'
: 0;
char valueBuf[262]; // Max 5 digits + decimal + 255 fraction digits + terminator
// The longest possible formatted number is fixed-point with 10 digits, 255 fractional digits,
// and a precision suffix, for 270 total bytes (counting the NUL terminator).
// (Actually 269 since a 2-digit precision cannot reach 10 integer digits.)
// Make the buffer somewhat larger just in case.
char valueBuf[300];
if (useType == 'b') {
// Special case for binary
char *ptr = valueBuf;
// Special case for binary (since `snprintf` doesn't support it)
// Buffer the digits from least to greatest
char *ptr = valueBuf;
do {
*ptr++ = (value & 1) + '0';
value >>= 1;
} while (value);
// Reverse the digits
// Reverse the digits and terminate the string
std::reverse(valueBuf, ptr);
*ptr = '\0';
} else if (useType == 'f') {
// Special case for fixed-point
// Special case for fixed-point (since it needs fractional part and precision)
// Default fractional width (C++'s is 6 for "%f"; here 5 is enough for Q16.16)
size_t useFracWidth = hasFrac ? fracWidth : 5;
@@ -226,6 +211,7 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
useFracWidth = 255;
}
// Default precision taken from default `-Q` option
size_t defaultPrec = options.fixPrecision;
size_t usePrec = hasPrec ? precision : defaultPrec;
if (usePrec < 1 || usePrec > 31) {
@@ -237,29 +223,30 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
usePrec = defaultPrec;
}
// Floating-point formatting works for all fixed-point values
double fval = fabs(value / pow(2.0, usePrec));
if (int fracWidthArg = static_cast<int>(useFracWidth); useExact) {
snprintf(valueBuf, sizeof(valueBuf), "%.*fq%zu", fracWidthArg, fval, usePrec);
} else {
snprintf(valueBuf, sizeof(valueBuf), "%.*f", fracWidthArg, fval);
}
} else if (useType == 'd') {
// Decimal numbers may be formatted with a '-' sign by `snprintf`, so `abs` prevents that,
// with a special case for `INT32_MIN` since `labs(INT32_MIN)` is UB. The sign will be
// printed later from `signChar`.
uint32_t uval =
value != static_cast<uint32_t>(INT32_MIN) ? labs(static_cast<int32_t>(value)) : value;
snprintf(valueBuf, sizeof(valueBuf), "%" PRIu32, uval);
} else {
char const *spec = useType == 'u' ? "%" PRIu32
// `value` has already been made non-negative, so type 'd' is OK here even for `INT32_MIN`.
// The sign will be printed later from `signChar`.
char const *spec = useType == 'd' || useType == 'u' ? "%" PRIu32
: useType == 'X' ? "%" PRIX32
: useType == 'x' ? "%" PRIx32
: useType == 'o' ? "%" PRIo32
: "%" PRIu32;
snprintf(valueBuf, sizeof(valueBuf), spec, value);
}
char prefixChar = !useExact ? 0
: useType == 'X' || useType == 'x' ? '$'
: useType == 'b' ? '%'
: useType == 'o' ? '&'
: 0;
size_t valueLen = strlen(valueBuf);
size_t numLen = (signChar != 0) + (prefixChar != 0) + valueLen;
size_t totalLen = width > numLen ? width : numLen;

View File

@@ -382,61 +382,54 @@ bool fstk_RunInclude(std::string const &path, bool isQuiet) {
return fstk_FileError(path, "INCLUDE");
}
static char const *suggestDef(std::shared_ptr<MacroArgs> const macroArgs) {
void fstk_RunMacro(
std::string const &macroName, std::shared_ptr<MacroArgs> macroArgs, bool isQuiet
) {
auto makeSuggestion = [&macroName, &macroArgs]() -> std::optional<std::string> {
std::shared_ptr<std::string> arg = macroArgs->getArg(1);
if (!arg) {
return nullptr;
return std::nullopt;
}
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;
for (char const *type : types) {
if (strncasecmp(str, type, strlen(type)) == 0) {
return "\"DEF "s + macroName + " " + type + " ...\"";
}
}
if (strncasecmp(str, "SET", literal_strlen("SET")) == 0) {
return "=";
return "\"DEF "s + macroName + " = ...\"";
}
if (str[0] == ':') {
return "a label \""s + macroName + (str[1] == ':' ? "::" : ":") + "\"";
}
return nullptr;
}
return std::nullopt;
};
void fstk_RunMacro(
std::string const &macroName, std::shared_ptr<MacroArgs> macroArgs, bool isQuiet
) {
Symbol *macro = sym_FindExactSymbol(macroName);
if (!macro) {
if (Symbol *macro = sym_FindExactSymbol(macroName); !macro) {
if (sym_IsPurgedExact(macroName)) {
error("Undefined macro `%s`; it was purged", macroName.c_str());
} else if (char const *defType = suggestDef(macroArgs); defType) {
} else if (std::optional<std::string> suggestion = makeSuggestion(); suggestion) {
error(
"Undefined macro `%s` (did you mean \"DEF %s %s ...\"?)",
macroName.c_str(),
macroName.c_str(),
defType
"Undefined macro `%s` (did you mean %s?)", macroName.c_str(), suggestion->c_str()
);
} else {
error("Undefined macro `%s`", macroName.c_str());
}
return;
}
if (macro->type != SYM_MACRO) {
} else if (macro->type != SYM_MACRO) {
error("`%s` is not a macro", macroName.c_str());
return;
}
} else {
newMacroContext(*macro, macroArgs, isQuiet || macro->isQuiet);
}
}
void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span, bool isQuiet) {
if (count == 0) {
return;
}
if (count) {
newReptContext(reptLineNo, span, count, isQuiet);
}
}
void fstk_RunFor(
std::string const &symName,

View File

@@ -879,7 +879,11 @@ static void discardBlockComment() {
continue;
case '/':
if (peek() == '*') {
warning(WARNING_NESTED_COMMENT, "\"/*\" in block comment");
warning(
WARNING_NESTED_COMMENT,
"\"/" // Prevent simple syntax highlighters from seeing this as a comment
"*\" in block comment"
);
}
continue;
case '*':
@@ -1009,6 +1013,10 @@ static bool isValidDigit(char c) {
return isAlphanumeric(c) || c == '.' || c == '#' || c == '@';
}
static bool isCustomBinDigit(int c) {
return isBinDigit(c) || c == options.binDigits[0] || c == options.binDigits[1];
}
static bool checkDigitErrors(char const *digits, size_t n, char const *type) {
for (size_t i = 0; i < n; ++i) {
char c = digits[i];
@@ -1074,10 +1082,7 @@ static uint32_t readBinaryNumber(char const *prefix) {
if (value > (UINT32_MAX - bit) / 2) {
warning(WARNING_LARGE_CONSTANT, "Integer constant is too large");
// Discard any additional digits
skipChars([](int d) {
return d == '0' || d == '1' || d == options.binDigits[0]
|| d == options.binDigits[1] || d == '_';
});
skipChars([](int d) { return isCustomBinDigit(d) || d == '_'; });
return 0;
}
value = value * 2 + bit;
@@ -1107,11 +1112,10 @@ static uint32_t readOctalNumber(char const *prefix) {
continue;
}
if (isOctDigit(c)) {
c = c - '0';
} else {
if (!isOctDigit(c)) {
break;
}
c = c - '0';
empty = false;
nonDigit = false;
@@ -1148,11 +1152,10 @@ static uint32_t readDecimalNumber(int initial) {
continue;
}
if (isDigit(c)) {
c = c - '0';
} else {
if (!isDigit(c)) {
break;
}
c = c - '0';
nonDigit = false;
if (value > (UINT32_MAX - c) / 10) {
@@ -1185,15 +1188,10 @@ static uint32_t readHexNumber(char const *prefix) {
continue;
}
if (c >= 'a' && c <= 'f') {
c = c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
c = c - 'A' + 10;
} else if (isDigit(c)) {
c = c - '0';
} else {
if (!isHexDigit(c)) {
break;
}
c = parseHexDigit(c);
empty = false;
nonDigit = false;
@@ -1756,14 +1754,22 @@ static Token yylex_NORMAL() {
case '^': // Either ^= or XOR
return oneOrTwo('=', T_(POP_XOREQ), T_(OP_XOR));
case '=': // Either assignment or EQ
return oneOrTwo('=', T_(OP_LOGICEQU), T_(POP_EQUAL));
case '!': // Either a NEQ or negation
return oneOrTwo('=', T_(OP_LOGICNE), T_(OP_LOGICNOT));
// Handle ambiguous 1-, 2-, or 3-char tokens
case '=': // Either assignment, EQ or string EQ
if (peek() == '=') {
shiftChar();
return oneOrTwo('=', T_(OP_STREQU), T_(OP_LOGICEQU));
}
return Token(T_(POP_EQUAL));
case '!': // Either negation, NEQ, or string NEQ
if (peek() == '=') {
shiftChar();
return oneOrTwo('=', T_(OP_STRNE), T_(OP_LOGICNE));
}
return Token(T_(OP_LOGICNOT));
case '<': // Either <<=, LT, LTE, or left shift
if (peek() == '<') {
shiftChar();
@@ -1834,8 +1840,7 @@ static Token yylex_NORMAL() {
case '%': // Either %=, MOD, or a binary constant
c = peek();
if (c == '0' || c == '1' || c == options.binDigits[0] || c == options.binDigits[1]
|| c == '_') {
if (isCustomBinDigit(c) || c == '_') {
return Token(T_(NUMBER), readBinaryNumber("'%'"));
}
return oneOrTwo('=', T_(POP_MODEQ), T_(OP_MOD));
@@ -1954,28 +1959,9 @@ static Token yylex_NORMAL() {
static Token yylex_RAW() {
// This is essentially a highly modified `readString`
std::string str;
size_t parenDepth = 0;
int c;
// Trim left spaces (stops at a block comment)
for (;;) {
c = peek();
if (isBlankSpace(c)) {
shiftChar();
} else if (c == '\\') {
c = nextChar();
// If not a line continuation, handle as a normal char
if (!isWhitespace(c)) {
goto backslash;
}
// Line continuations count as "space"
discardLineContinuation();
} else {
break;
}
}
for (;;) {
for (size_t parenDepth = 0;;) {
c = peek();
switch (c) {
@@ -2036,7 +2022,6 @@ static Token yylex_RAW() {
case '\\': // Character escape
c = nextChar();
backslash:
switch (c) {
case ',': // Escapes only valid inside a macro arg
case '(':
@@ -2091,9 +2076,9 @@ append:
}
finish: // Can't `break` out of a nested `for`-`switch`
// Trim right blank space
auto rightPos = std::find_if_not(str.rbegin(), str.rend(), isBlankSpace);
str.resize(rightPos.base() - str.begin());
// Trim left and right blank space
str.erase(str.begin(), std::find_if_not(RANGE(str), isBlankSpace));
str.erase(std::find_if_not(RRANGE(str), isBlankSpace).base(), str.end());
// Returning COMMAs to the parser would mean that two consecutive commas
// (i.e. an empty argument) need to return two different tokens (STRING

View File

@@ -6,6 +6,7 @@
#include <errno.h>
#include <inttypes.h>
#include <memory>
#include <optional>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
@@ -299,13 +300,15 @@ int main(int argc, char *argv[]) {
// https://reproducible-builds.org/docs/source-date-epoch/
time_t now = time(nullptr);
if (char const *sourceDateEpoch = getenv("SOURCE_DATE_EPOCH"); sourceDateEpoch) {
// Use `strtoul`, not `parseWholeNumber`, because SOURCE_DATE_EPOCH does
// not conventionally support our custom base prefixes
now = static_cast<time_t>(strtoul(sourceDateEpoch, nullptr, 0));
}
sym_Init(now);
// Maximum of 100 errors only applies if rgbasm is printing errors to a terminal
if (isatty(STDERR_FILENO)) {
options.maxErrors = 100;
options.maxErrors = 100; // LCOV_EXCL_LINE
}
// Parse CLI options
@@ -378,51 +381,41 @@ int main(int argc, char *argv[]) {
fstk_AddPreIncludeFile(musl_optarg);
break;
case 'p': {
char *endptr;
unsigned long padByte = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
case 'p':
if (std::optional<uint64_t> padByte = parseWholeNumber(musl_optarg); !padByte) {
fatal("Invalid argument for option '-p'");
}
if (padByte > 0xFF) {
} else if (*padByte > 0xFF) {
fatal("Argument for option '-p' must be between 0 and 0xFF");
} else {
opt_P(*padByte);
}
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') {
if (std::optional<uint64_t> precision = parseWholeNumber(precisionArg); !precision) {
fatal("Invalid argument for option '-Q'");
}
if (precision < 1 || precision > 31) {
} else if (*precision < 1 || *precision > 31) {
fatal("Argument for option '-Q' must be between 1 and 31");
} else {
opt_Q(*precision);
}
opt_Q(precision);
break;
}
case 'r': {
char *endptr;
options.maxRecursionDepth = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
case 'r':
if (std::optional<uint64_t> maxDepth = parseWholeNumber(musl_optarg); !maxDepth) {
fatal("Invalid argument for option '-r'");
} else if (errno == ERANGE) {
fatal("Argument for option '-r' is out of range");
} else {
options.maxRecursionDepth = *maxDepth;
}
break;
}
case 's': {
// Split "<features>:<name>" so `musl_optarg` is "<features>" and `name` is "<name>"
@@ -459,21 +452,15 @@ int main(int argc, char *argv[]) {
warnings.state.warningsEnabled = false;
break;
case 'X': {
char *endptr;
uint64_t maxErrors = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
case 'X':
if (std::optional<uint64_t> maxErrors = parseWholeNumber(musl_optarg); !maxErrors) {
fatal("Invalid argument for option '-X'");
}
if (maxErrors > UINT64_MAX) {
} else if (*maxErrors > UINT64_MAX) {
fatal("Argument for option '-X' must be between 0 and %" PRIu64, UINT64_MAX);
} else {
options.maxErrors = *maxErrors;
}
options.maxErrors = maxErrors;
break;
}
case 0: // Long-only options
switch (longOpt) {
@@ -510,9 +497,10 @@ int main(int argc, char *argv[]) {
}
break;
// Unrecognized options
// LCOV_EXCL_START
default:
usage.printAndExit(1); // LCOV_EXCL_LINE
usage.printAndExit(1);
// LCOV_EXCL_STOP
}
}
@@ -533,6 +521,11 @@ int main(int argc, char *argv[]) {
verbosePrint(VERB_NOTICE, "Assembling \"%s\"\n", mainFileName.c_str()); // LCOV_EXCL_LINE
if (dependFileName) {
if (options.targetFileName.empty()) {
fatal("Dependency files can only be created if a target file is specified with either "
"'-o', '-MQ' or '-MT'");
}
if (strcmp("-", dependFileName)) {
options.dependFile = fopen(dependFileName, "w");
if (options.dependFile == nullptr) {
@@ -545,10 +538,6 @@ int main(int argc, char *argv[]) {
}
}
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'");
}
options.printDep(mainFileName);
charmap_New(DEFAULT_CHARMAP_NAME, nullptr);
@@ -558,12 +547,16 @@ int main(int argc, char *argv[]) {
// Perform parse (`yy::parser` is auto-generated from `parser.y`)
if (yy::parser parser; parser.parse() != 0) {
if (warnings.nbErrors == 0) {
warnings.nbErrors = 1;
}
// Exited due to YYABORT or YYNOMEM
fatal("Unrecoverable error while parsing"); // LCOV_EXCL_LINE
}
// If parse aborted without errors due to a missing INCLUDE, and `-MG` was given, exit normally
if (fstk_FailedOnMissingInclude()) {
requireZeroErrors();
return 0;
}
if (!fstk_FailedOnMissingInclude()) {
sect_CheckUnionClosed();
sect_CheckLoadClosed();
sect_CheckSizes();
@@ -571,15 +564,9 @@ int main(int argc, char *argv[]) {
charmap_CheckStack();
opt_CheckStack();
sect_CheckStack();
}
requireZeroErrors();
// If parse aborted due to missing an include, and `-MG` was given, exit normally
if (fstk_FailedOnMissingInclude()) {
return 0;
}
out_WriteObject();
for (auto const &[name, features] : stateFileSpecs) {

View File

@@ -2,6 +2,7 @@
#include <errno.h>
#include <iterator> // std::size
#include <optional>
#include <stack>
#include <stdint.h>
#include <stdio.h>
@@ -9,8 +10,7 @@
#include <string.h>
#include "diagnostics.hpp"
#include "helpers.hpp" // assume
#include "util.hpp" // isBlankSpace
#include "util.hpp"
#include "asm/fstack.hpp"
#include "asm/lexer.hpp"
@@ -55,100 +55,75 @@ void opt_W(char const *flag) {
void opt_Parse(char const *s) {
if (s[0] == '-') {
++s;
++s; // Skip a leading '-'
}
switch (s[0]) {
char c = *s++;
while (isBlankSpace(*s)) {
++s; // Skip leading blank spaces
}
switch (c) {
case 'b':
if (strlen(&s[1]) == 2) {
opt_B(&s[1]);
if (strlen(s) == 2) {
opt_B(s);
} else {
error("Must specify exactly 2 characters for option 'b'");
}
break;
case 'g':
if (strlen(&s[1]) == 4) {
opt_G(&s[1]);
if (strlen(s) == 4) {
opt_G(s);
} else {
error("Must specify exactly 4 characters for option 'g'");
}
break;
case 'p':
if (strlen(&s[1]) <= 2) {
int result;
unsigned int padByte;
result = sscanf(&s[1], "%x", &padByte);
if (result != 1) {
if (std::optional<uint64_t> padByte = parseWholeNumber(s); !padByte) {
error("Invalid argument for option 'p'");
} else if (*padByte > 0xFF) {
error("Argument for option 'p' must be between 0 and 0xFF");
} else {
// Two characters cannot be scanned as a hex number greater than 0xFF
assume(padByte <= 0xFF);
opt_P(padByte);
}
} else {
error("Invalid argument for option 'p'");
opt_P(*padByte);
}
break;
case 'Q': {
char const *precisionArg = &s[1];
if (precisionArg[0] == '.') {
++precisionArg;
case 'Q':
if (s[0] == '.') {
++s; // Skip leading '.'
}
if (strlen(precisionArg) <= 2) {
int result;
unsigned int fixPrecision;
result = sscanf(precisionArg, "%u", &fixPrecision);
if (result != 1) {
if (std::optional<uint64_t> precision = parseWholeNumber(s); !precision) {
error("Invalid argument for option 'Q'");
} else if (fixPrecision < 1 || fixPrecision > 31) {
} else if (*precision < 1 || *precision > 31) {
error("Argument for option 'Q' must be between 1 and 31");
} else {
opt_Q(fixPrecision);
}
} else {
error("Invalid argument for option 'Q'");
opt_Q(*precision);
}
break;
}
case 'r': {
++s; // Skip 'r'
while (isBlankSpace(*s)) {
++s; // Skip leading blank spaces
}
if (s[0] == '\0') {
error("Missing argument for option 'r'");
break;
}
char *endptr;
unsigned long maxRecursionDepth = strtoul(s, &endptr, 10);
if (*endptr != '\0') {
error("Invalid argument for option 'r' (\"%s\")", s);
case 'r':
if (std::optional<uint64_t> maxRecursionDepth = parseWholeNumber(s); !maxRecursionDepth) {
error("Invalid argument for option 'r'");
} else if (errno == ERANGE) {
error("Argument for option 'r' is out of range (\"%s\")", s);
error("Argument for option 'r' is out of range");
} else {
opt_R(maxRecursionDepth);
opt_R(*maxRecursionDepth);
}
break;
}
case 'W':
if (strlen(&s[1]) > 0) {
opt_W(&s[1]);
if (strlen(s) > 0) {
opt_W(s);
} else {
error("Must specify an argument for option 'W'");
}
break;
default:
error("Unknown option '%c'", s[0]);
error("Unknown option '%c'", c);
break;
}
}

View File

@@ -123,7 +123,7 @@ static void writeSymbol(Symbol const &sym, FILE *file) {
}
}
static void registerUnregisteredSymbol(Symbol &sym) {
void out_RegisterSymbol(Symbol &sym) {
// Check for `sym.src`, to skip any built-in symbol from rgbasm
if (sym.src && sym.ID == UINT32_MAX && !sym_IsPC(&sym)) {
sym.ID = objectSymbols.size(); // Set the symbol's ID within the object file
@@ -132,90 +132,6 @@ static void registerUnregisteredSymbol(Symbol &sym) {
}
}
static void writeRpn(std::vector<uint8_t> &rpnexpr, std::vector<uint8_t> const &rpn) {
size_t rpnptr = 0;
for (size_t offset = 0; offset < rpn.size();) {
uint8_t rpndata = rpn[offset++];
auto getSymName = [&]() {
std::string symName;
for (uint8_t c; (c = rpn[offset++]) != 0;) {
symName += c;
}
return symName;
};
switch (rpndata) {
Symbol *sym;
uint32_t value;
uint8_t b;
case RPN_CONST:
rpnexpr[rpnptr++] = RPN_CONST;
rpnexpr[rpnptr++] = rpn[offset++];
rpnexpr[rpnptr++] = rpn[offset++];
rpnexpr[rpnptr++] = rpn[offset++];
rpnexpr[rpnptr++] = rpn[offset++];
break;
case RPN_SYM:
// The symbol name is always written expanded
sym = sym_FindExactSymbol(getSymName());
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;
rpnexpr[rpnptr++] = value >> 24;
break;
case RPN_BANK_SYM:
// The symbol name is always written expanded
sym = sym_FindExactSymbol(getSymName());
registerUnregisteredSymbol(*sym); // Ensure that `sym->ID` is set
value = sym->ID;
rpnexpr[rpnptr++] = RPN_BANK_SYM;
rpnexpr[rpnptr++] = value & 0xFF;
rpnexpr[rpnptr++] = value >> 8;
rpnexpr[rpnptr++] = value >> 16;
rpnexpr[rpnptr++] = value >> 24;
break;
case RPN_BANK_SECT:
rpnexpr[rpnptr++] = RPN_BANK_SECT;
do {
b = rpn[offset++];
rpnexpr[rpnptr++] = b;
} while (b != 0);
break;
case RPN_SIZEOF_SECT:
rpnexpr[rpnptr++] = RPN_SIZEOF_SECT;
do {
b = rpn[offset++];
rpnexpr[rpnptr++] = b;
} while (b != 0);
break;
case RPN_STARTOF_SECT:
rpnexpr[rpnptr++] = RPN_STARTOF_SECT;
do {
b = rpn[offset++];
rpnexpr[rpnptr++] = b;
} while (b != 0);
break;
default:
rpnexpr[rpnptr++] = rpndata;
break;
}
}
}
static void initPatch(Patch &patch, uint32_t type, Expression const &expr, uint32_t ofs) {
patch.type = type;
patch.src = fstk_GetFileStack();
@@ -225,20 +141,7 @@ static void initPatch(Patch &patch, uint32_t type, Expression const &expr, uint3
patch.offset = ofs;
patch.pcSection = sect_GetSymbolSection();
patch.pcOffset = sect_GetSymbolOffset();
if (expr.isKnown()) {
// If the RPN expr's value is known, output a constant directly
uint32_t val = expr.value();
patch.rpn.resize(5);
patch.rpn[0] = RPN_CONST;
patch.rpn[1] = val & 0xFF;
patch.rpn[2] = val >> 8;
patch.rpn[3] = val >> 16;
patch.rpn[4] = val >> 24;
} else {
patch.rpn.resize(expr.rpnPatchSize);
writeRpn(patch.rpn, expr.rpn);
}
expr.encode(patch.rpn);
}
void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32_t pcShift) {
@@ -310,7 +213,7 @@ void out_WriteObject() {
Defer closeFile{[&] { fclose(file); }};
// Also write symbols that weren't written above
sym_ForEach(registerUnregisteredSymbol);
sym_ForEach(out_RegisterSymbol);
fputs(RGBDS_OBJECT_VERSION_STRING, file);
putLong(RGBDS_OBJECT_REV, file);

View File

@@ -107,6 +107,7 @@
// String operators
%token OP_CAT "++"
%token OP_STREQU "===" OP_STRNE "!=="
// Comparison operators
%token OP_LOGICEQU "==" OP_LOGICNE "!="
@@ -1049,6 +1050,9 @@ charmap:
POP_CHARMAP string COMMA charmap_args trailing_comma {
charmap_Add($2, std::move($4));
}
| POP_CHARMAP CHARACTER COMMA charmap_args trailing_comma {
charmap_Add($2, std::move($4));
}
;
charmap_args:
@@ -1284,6 +1288,12 @@ relocexpr_no_str:
| CHARACTER {
$$.makeNumber(act_CharToNum($1));
}
| string OP_STREQU string {
$$.makeNumber($1.compare($3) == 0);
}
| string OP_STRNE string {
$$.makeNumber($1.compare($3) != 0);
}
| OP_LOGICNOT relocexpr %prec NEG {
$$.makeUnaryOp(RPN_LOGNOT, std::move($2));
}
@@ -1891,7 +1901,7 @@ sm83_and:
sm83_bit:
SM83_BIT reloc_3bit COMMA reg_r {
uint8_t mask = static_cast<uint8_t>(0x40 | $4);
$2.makeCheckBitIndex(mask);
$2.addCheckBitIndex(mask);
sect_ConstByte(0xCB);
if (!$2.isKnown()) {
sect_RelByte($2, 0);
@@ -2024,7 +2034,7 @@ sm83_ldd:
sm83_ldh:
SM83_LDH MODE_A COMMA op_mem_ind {
$4.makeCheckHRAM();
$4.addCheckHRAM();
sect_ConstByte(0xF0);
if (!$4.isKnown()) {
sect_RelByte($4, 1);
@@ -2033,7 +2043,7 @@ sm83_ldh:
}
}
| SM83_LDH op_mem_ind COMMA MODE_A {
$2.makeCheckHRAM();
$2.addCheckHRAM();
sect_ConstByte(0xE0);
if (!$2.isKnown()) {
sect_RelByte($2, 1);
@@ -2217,7 +2227,7 @@ sm83_push:
sm83_res:
SM83_RES reloc_3bit COMMA reg_r {
uint8_t mask = static_cast<uint8_t>(0x80 | $4);
$2.makeCheckBitIndex(mask);
$2.addCheckBitIndex(mask);
sect_ConstByte(0xCB);
if (!$2.isKnown()) {
sect_RelByte($2, 0);
@@ -2296,7 +2306,7 @@ sm83_rrca:
sm83_rst:
SM83_RST reloc_8bit {
$2.makeCheckRST();
$2.addCheckRST();
if (!$2.isKnown()) {
sect_RelByte($2, 0);
} else {
@@ -2324,7 +2334,7 @@ sm83_scf:
sm83_set:
SM83_SET reloc_3bit COMMA reg_r {
uint8_t mask = static_cast<uint8_t>(0xC0 | $4);
$2.makeCheckBitIndex(mask);
$2.addCheckBitIndex(mask);
sect_ConstByte(0xCB);
if (!$2.isKnown()) {
sect_RelByte($2, 0);

View File

@@ -8,10 +8,11 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include "helpers.hpp" // assume
#include "linkdefs.hpp"
@@ -24,24 +25,6 @@
using namespace std::literals;
void Expression::clear() {
data = 0;
isSymbol = false;
rpn.clear();
rpnPatchSize = 0;
}
uint8_t *Expression::reserveSpace(uint32_t size) {
return reserveSpace(size, size);
}
uint8_t *Expression::reserveSpace(uint32_t size, uint32_t patchSize) {
rpnPatchSize += patchSize;
size_t curSize = rpn.size();
rpn.resize(curSize + size);
return &rpn[curSize];
}
int32_t Expression::getConstVal() const {
if (!isKnown()) {
error("Expected constant expression: %s", std::get<std::string>(data).c_str());
@@ -51,10 +34,10 @@ int32_t Expression::getConstVal() const {
}
Symbol const *Expression::symbolOf() const {
if (!isSymbol) {
if (rpn.size() != 1 || rpn[0].command != RPN_SYM) {
return nullptr;
}
return sym_FindScopedSymbol(reinterpret_cast<char const *>(&rpn[1]));
return sym_FindScopedSymbol(std::get<std::string>(rpn[0].data));
}
bool Expression::isDiffConstant(Symbol const *sym) const {
@@ -71,12 +54,12 @@ bool Expression::isDiffConstant(Symbol const *sym) const {
}
void Expression::makeNumber(uint32_t value) {
clear();
assume(rpn.empty());
data = static_cast<int32_t>(value);
}
void Expression::makeSymbol(std::string const &symName) {
clear();
assume(rpn.empty());
if (Symbol *sym = sym_FindScopedSymbol(symName); sym_IsPC(sym) && !sect_GetSymbolSection()) {
error("PC has no value outside of a section");
data = 0;
@@ -84,28 +67,20 @@ void Expression::makeSymbol(std::string const &symName) {
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 && sym->isDefined()
? "`"s + symName + "` is not constant at assembly time"
: "undefined symbol `"s + symName + "`")
+ (sym_IsPurgedScoped(symName) ? "; it was purged" : "");
sym = sym_Ref(symName);
size_t nameLen = sym->name.length() + 1; // Don't forget NUL!
// 1-byte opcode + 4-byte symbol ID
uint8_t *ptr = reserveSpace(nameLen + 1, 5);
*ptr++ = RPN_SYM;
memcpy(ptr, sym->name.c_str(), nameLen);
rpn.emplace_back(RPN_SYM, sym->name);
} else {
data = static_cast<int32_t>(sym->getConstantValue());
}
}
void Expression::makeBankSymbol(std::string const &symName) {
clear();
assume(rpn.empty());
if (Symbol const *sym = sym_FindScopedSymbol(symName); sym_IsPC(sym)) {
// The @ symbol is treated differently.
if (std::optional<uint32_t> outputBank = sect_GetOutputBank(); !outputBank) {
@@ -113,19 +88,16 @@ void Expression::makeBankSymbol(std::string const &symName) {
data = 1;
} else if (*outputBank == UINT32_MAX) {
data = "Current section's bank is not known";
*reserveSpace(1) = RPN_BANK_SELF;
rpn.emplace_back(RPN_BANK_SELF);
} else {
data = static_cast<int32_t>(*outputBank);
}
return;
} else if (sym && !sym->isLabel()) {
error("`BANK` argument must be a label");
data = 1;
} else {
sym = sym_Ref(symName);
assume(sym); // If the symbol didn't exist, it should have been created
if (sym->getSection() && sym->getSection()->bank != UINT32_MAX) {
// Symbol's section is known and bank is fixed
data = static_cast<int32_t>(sym->getSection()->bank);
@@ -133,78 +105,51 @@ void Expression::makeBankSymbol(std::string const &symName) {
data = sym_IsPurgedScoped(symName)
? "`"s + symName + "`'s bank is not known; it was purged"
: "`"s + symName + "`'s bank is not known";
size_t nameLen = sym->name.length() + 1; // Room for NUL!
// 1-byte opcode + 4-byte sect ID
uint8_t *ptr = reserveSpace(nameLen + 1, 5);
*ptr++ = RPN_BANK_SYM;
memcpy(ptr, sym->name.c_str(), nameLen);
rpn.emplace_back(RPN_BANK_SYM, sym->name);
}
}
}
void Expression::makeBankSection(std::string const &sectName) {
clear();
assume(rpn.empty());
if (Section *sect = sect_FindSectionByName(sectName); sect && sect->bank != UINT32_MAX) {
data = static_cast<int32_t>(sect->bank);
} else {
data = "Section \""s + sectName + "\"'s bank is not known";
size_t nameLen = sectName.length() + 1; // Room for NUL!
uint8_t *ptr = reserveSpace(nameLen + 1);
*ptr++ = RPN_BANK_SECT;
memcpy(ptr, sectName.data(), nameLen);
rpn.emplace_back(RPN_BANK_SECT, sectName);
}
}
void Expression::makeSizeOfSection(std::string const &sectName) {
clear();
assume(rpn.empty());
if (Section *sect = sect_FindSectionByName(sectName); sect && sect->isSizeKnown()) {
data = static_cast<int32_t>(sect->size);
} else {
data = "Section \""s + sectName + "\"'s size is not known";
size_t nameLen = sectName.length() + 1; // Room for NUL!
uint8_t *ptr = reserveSpace(nameLen + 1);
*ptr++ = RPN_SIZEOF_SECT;
memcpy(ptr, sectName.data(), nameLen);
rpn.emplace_back(RPN_SIZEOF_SECT, sectName);
}
}
void Expression::makeStartOfSection(std::string const &sectName) {
clear();
assume(rpn.empty());
if (Section *sect = sect_FindSectionByName(sectName); sect && sect->org != UINT32_MAX) {
data = static_cast<int32_t>(sect->org);
} else {
data = "Section \""s + sectName + "\"'s start is not known";
size_t nameLen = sectName.length() + 1; // Room for NUL!
uint8_t *ptr = reserveSpace(nameLen + 1);
*ptr++ = RPN_STARTOF_SECT;
memcpy(ptr, sectName.data(), nameLen);
rpn.emplace_back(RPN_STARTOF_SECT, sectName);
}
}
void Expression::makeSizeOfSectionType(SectionType type) {
clear();
assume(rpn.empty());
data = "Section type's size is not known";
uint8_t *ptr = reserveSpace(2);
*ptr++ = RPN_SIZEOF_SECTTYPE;
*ptr = type;
rpn.emplace_back(RPN_SIZEOF_SECTTYPE, static_cast<uint8_t>(type));
}
void Expression::makeStartOfSectionType(SectionType type) {
clear();
assume(rpn.empty());
data = "Section type's start is not known";
uint8_t *ptr = reserveSpace(2);
*ptr++ = RPN_STARTOF_SECTTYPE;
*ptr = type;
rpn.emplace_back(RPN_STARTOF_SECTTYPE, static_cast<uint8_t>(type));
}
static bool tryConstZero(Expression const &lhs, Expression const &rhs) {
@@ -302,7 +247,7 @@ static int32_t tryConstMask(Expression const &lhs, Expression const &rhs) {
}
void Expression::makeUnaryOp(RPNCommand op, Expression &&src) {
clear();
assume(rpn.empty());
// First, check if the expression is known
if (src.isKnown()) {
// If the expressions is known, just compute the value
@@ -339,16 +284,15 @@ void Expression::makeUnaryOp(RPNCommand op, Expression &&src) {
} else if (int32_t constVal; op == RPN_LOW && (constVal = tryConstLow(src)) != -1) {
data = constVal;
} else {
// If it's not known, just reuse its RPN buffer and append the operator
rpnPatchSize = src.rpnPatchSize;
std::swap(rpn, src.rpn);
// If it's not known, just reuse its RPN vector and append the operator
data = std::move(src.data);
*reserveSpace(1) = op;
std::swap(rpn, src.rpn);
rpn.emplace_back(op);
}
}
void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const &src2) {
clear();
assume(rpn.empty());
// First, check if the expressions are known
if (src1.isKnown() && src2.isKnown()) {
// If both expressions are known, just compute the value
@@ -480,57 +424,32 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
// Convert the left-hand expression if it's constant
if (src1.isKnown()) {
uint32_t lval = src1.value();
uint8_t bytes[] = {
RPN_CONST,
static_cast<uint8_t>(lval),
static_cast<uint8_t>(lval >> 8),
static_cast<uint8_t>(lval >> 16),
static_cast<uint8_t>(lval >> 24),
};
rpn.clear();
rpnPatchSize = 0;
memcpy(reserveSpace(sizeof(bytes)), bytes, sizeof(bytes));
// Use the other expression's un-const reason
data = std::move(src2.data);
rpn.emplace_back(RPN_CONST, lval);
} else {
// Otherwise just reuse its RPN buffer
rpnPatchSize = src1.rpnPatchSize;
std::swap(rpn, src1.rpn);
// Otherwise just reuse its RPN vector
data = std::move(src1.data);
std::swap(rpn, src1.rpn);
}
// Now, merge the right expression into the left one
if (src2.isKnown()) {
// If the right expression is constant, append a shim instead
// If the right expression is constant, append its value
uint32_t rval = src2.value();
uint8_t bytes[] = {
RPN_CONST,
static_cast<uint8_t>(rval),
static_cast<uint8_t>(rval >> 8),
static_cast<uint8_t>(rval >> 16),
static_cast<uint8_t>(rval >> 24),
};
uint8_t *ptr = reserveSpace(sizeof(bytes) + 1, sizeof(bytes) + 1);
memcpy(ptr, bytes, sizeof(bytes));
ptr[sizeof(bytes)] = op;
rpn.emplace_back(RPN_CONST, rval);
} else {
// Copy the right RPN and append the operator
uint32_t rightRpnSize = src2.rpn.size();
uint8_t *ptr = reserveSpace(rightRpnSize + 1, src2.rpnPatchSize + 1);
if (rightRpnSize > 0) {
// If `rightRpnSize == 0`, then `memcpy(ptr, nullptr, rightRpnSize)` would be UB
memcpy(ptr, src2.rpn.data(), rightRpnSize);
}
ptr[rightRpnSize] = op;
// Otherwise just extend with its RPN vector
rpn.insert(rpn.end(), RANGE(src2.rpn));
}
// Append the operator
rpn.emplace_back(op);
}
}
void Expression::makeCheckHRAM() {
isSymbol = false;
void Expression::addCheckHRAM() {
if (!isKnown()) {
*reserveSpace(1) = RPN_HRAM;
rpn.emplace_back(RPN_HRAM);
} else if (int32_t val = value(); val >= 0xFF00 && val <= 0xFFFF) {
// That range is valid; only keep the lower byte
data = val & 0xFF;
@@ -539,22 +458,19 @@ void Expression::makeCheckHRAM() {
}
}
void Expression::makeCheckRST() {
void Expression::addCheckRST() {
if (!isKnown()) {
*reserveSpace(1) = RPN_RST;
rpn.emplace_back(RPN_RST);
} else if (int32_t val = value(); val & ~0x38) {
// A valid RST address must be masked with 0x38
error("Invalid address $%" PRIx32 " for `RST`", val);
}
}
void Expression::makeCheckBitIndex(uint8_t mask) {
void Expression::addCheckBitIndex(uint8_t mask) {
assume((mask & 0xC0) != 0x00); // The high two bits must correspond to BIT, RES, or SET
if (!isKnown()) {
uint8_t *ptr = reserveSpace(2);
*ptr++ = RPN_BIT_INDEX;
*ptr = mask;
rpn.emplace_back(RPN_BIT_INDEX, mask);
} else if (int32_t val = value(); val & ~0x07) {
// A valid bit index must be masked with 0x07
static char const *instructions[4] = {"instruction", "`BIT`", "`RES`", "`SET`"};
@@ -596,3 +512,104 @@ bool checkNBit(int32_t v, uint8_t n, char const *name) {
return true;
}
void Expression::encode(std::vector<uint8_t> &buffer) const {
assume(buffer.empty());
if (isKnown()) {
// If the RPN expression's value is known, output a constant directly
uint32_t val = value();
buffer.resize(5);
buffer[0] = RPN_CONST;
buffer[1] = val & 0xFF;
buffer[2] = val >> 8;
buffer[3] = val >> 16;
buffer[4] = val >> 24;
} else {
// If the RPN expression's value is not known, serialize its RPN values
buffer.reserve(rpn.size() * 2); // Rough estimate of the serialized size
for (RPNValue const &val : rpn) {
val.appendEncoded(buffer);
}
}
}
RPNValue::RPNValue(RPNCommand cmd) : command(cmd), data(std::monostate{}) {
assume(
cmd != RPN_SIZEOF_SECTTYPE && cmd != RPN_STARTOF_SECTTYPE && cmd != RPN_BIT_INDEX
&& cmd != RPN_CONST && cmd != RPN_SYM && cmd != RPN_BANK_SYM && cmd != RPN_BANK_SECT
&& cmd != RPN_SIZEOF_SECT && cmd != RPN_STARTOF_SECT
);
}
RPNValue::RPNValue(RPNCommand cmd, uint8_t val) : command(cmd), data(val) {
assume(cmd == RPN_SIZEOF_SECTTYPE || cmd == RPN_STARTOF_SECTTYPE || cmd == RPN_BIT_INDEX);
}
RPNValue::RPNValue(RPNCommand cmd, uint32_t val) : command(cmd), data(val) {
assume(cmd == RPN_CONST);
}
RPNValue::RPNValue(RPNCommand cmd, std::string const &name) : command(cmd), data(name) {
assume(
cmd == RPN_SYM || cmd == RPN_BANK_SYM || cmd == RPN_BANK_SECT || cmd == RPN_SIZEOF_SECT
|| cmd == RPN_STARTOF_SECT
);
}
void RPNValue::appendEncoded(std::vector<uint8_t> &buffer) const {
// Every command starts with its own ID
buffer.push_back(command);
switch (command) {
case RPN_CONST: {
// The command ID is followed by a four-byte integer
assume(std::holds_alternative<uint32_t>(data));
uint32_t val = std::get<uint32_t>(data);
buffer.push_back(val & 0xFF);
buffer.push_back(val >> 8);
buffer.push_back(val >> 16);
buffer.push_back(val >> 24);
break;
}
case RPN_SYM:
case RPN_BANK_SYM: {
// The command ID is followed by a four-byte symbol ID
assume(std::holds_alternative<std::string>(data));
// The symbol name is always written expanded
Symbol *sym = sym_FindExactSymbol(std::get<std::string>(data));
out_RegisterSymbol(*sym); // Ensure that `sym->ID` is set
buffer.push_back(sym->ID & 0xFF);
buffer.push_back(sym->ID >> 8);
buffer.push_back(sym->ID >> 16);
buffer.push_back(sym->ID >> 24);
break;
}
case RPN_BANK_SECT:
case RPN_SIZEOF_SECT:
case RPN_STARTOF_SECT: {
// The command ID is followed by a NUL-terminated section name string
assume(std::holds_alternative<std::string>(data));
std::string const &name = std::get<std::string>(data);
buffer.reserve(buffer.size() + name.length() + 1);
buffer.insert(buffer.end(), RANGE(name));
buffer.push_back('\0');
break;
}
case RPN_SIZEOF_SECTTYPE:
case RPN_STARTOF_SECTTYPE:
case RPN_BIT_INDEX:
// The command ID is followed by a byte value
assume(std::holds_alternative<uint8_t>(data));
buffer.push_back(std::get<uint8_t>(data));
break;
default:
// Other command IDs are not followed by anything
assume(std::holds_alternative<std::monostate>(data));
break;
}
}

View File

@@ -218,14 +218,8 @@ static unsigned int
}
} else if (alignment != 0) {
int32_t curOfs = (alignOffset - sect.size) % alignSize;
if (curOfs < 0) {
curOfs += alignSize;
}
// Make sure any fixed address given is compatible
if (sect.org != UINT32_MAX) {
if (uint32_t curOfs = (alignOffset - sect.size) & alignMask; sect.org != UINT32_MAX) {
if ((sect.org - curOfs) & alignMask) {
sectError(
"Section already declared as fixed at incompatible address $%04" PRIx32,
@@ -406,13 +400,16 @@ static Section *getSection(
bank = sectionTypeInfo[type].firstBank;
}
// This should be redundant, as the parser guarantees that `AlignmentSpec` will be valid.
if (alignOffset >= alignSize) {
// LCOV_EXCL_START
error(
"Alignment offset (%" PRIu16 ") must be smaller than alignment size (%" PRIu32 ")",
alignOffset,
alignSize
);
alignOffset = 0;
// LCOV_EXCL_STOP
}
if (org != UINT32_MAX) {
@@ -621,10 +618,10 @@ uint32_t sect_GetAlignBytes(uint8_t alignment, uint16_t offset) {
return 0;
}
// We need `(pcValue + curOffset + return value) % (1 << alignment) == offset`
// We need `(pcValue + curOffset + return value) & minAlignMask == offset`
uint16_t pcValue = isFixed ? sect->org : sect->alignOfs;
return static_cast<uint16_t>(offset - curOffset - pcValue)
% (1u << std::min(alignment, curAlignment));
uint32_t minAlignMask = (1u << std::min(alignment, curAlignment)) - 1;
return static_cast<uint16_t>(offset - curOffset - pcValue) & minAlignMask;
}
void sect_AlignPC(uint8_t alignment, uint16_t offset) {
@@ -634,13 +631,15 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
assume(alignment <= 16); // Should be ensured by the caller
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
Section *sect = sect_GetSymbolSection();
assume(sect->align <= 16); // Left-shifting by 32 or more would be UB
uint32_t sectAlignSize = 1u << sect->align;
uint32_t sectAlignMask = sectAlignSize - 1;
if (sect->org != UINT32_MAX) {
if (uint32_t actualOffset = (sect->org + curOffset) % alignSize; actualOffset != offset) {
if (uint32_t actualOffset = (sect->org + curOffset) & alignMask; actualOffset != offset) {
error(
"Section is misaligned (at PC = $%04" PRIx32 ", expected ALIGN[%" PRIu32
", %" PRIu32 "], got ALIGN[%" PRIu32 ", %" PRIu32 "])",
@@ -651,8 +650,8 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
actualOffset
);
}
} else if (uint32_t actualOffset = (sect->alignOfs + curOffset) % alignSize;
sect->align != 0 && actualOffset % sectAlignSize != offset % sectAlignSize) {
} else if (uint32_t actualOffset = (sect->alignOfs + curOffset) & alignMask;
sect->align != 0 && (actualOffset & sectAlignMask) != (offset & sectAlignMask)) {
error(
"Section is misaligned ($%04" PRIx32 " bytes into the section, expected ALIGN[%" PRIu32
", %" PRIu32 "], got ALIGN[%" PRIu32 ", %" PRIu32 "])",
@@ -669,8 +668,8 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
sect->org = offset - curOffset;
} else if (alignment > sect->align) {
sect->align = alignment;
// We need `(sect->alignOfs + curOffset) % alignSize == offset`
sect->alignOfs = (offset - curOffset) % alignSize;
// We need `(sect->alignOfs + curOffset) & alignMask == offset`
sect->alignOfs = (offset - curOffset) & alignMask;
}
}
@@ -945,12 +944,11 @@ bool sect_BinaryFile(std::string const &name, uint32_t startPos) {
// The file is seekable; skip to the specified start position
fseek(file, startPos, SEEK_SET);
} else {
if (errno != ESPIPE) {
// LCOV_EXCL_START
if (errno != ESPIPE) {
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--) {
@@ -961,6 +959,7 @@ bool sect_BinaryFile(std::string const &name, uint32_t startPos) {
return false;
}
}
// LCOV_EXCL_STOP
}
for (int byte; (byte = fgetc(file)) != EOF;) {
@@ -1010,12 +1009,11 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l
// The file is seekable; skip to the specified start position
fseek(file, startPos, SEEK_SET);
} else {
if (errno != ESPIPE) {
// LCOV_EXCL_START
if (errno != ESPIPE) {
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--) {
@@ -1026,21 +1024,22 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l
return false;
}
}
// LCOV_EXCL_STOP
}
while (length--) {
if (int byte = fgetc(file); byte != EOF) {
writeByte(byte);
} else if (ferror(file)) {
// LCOV_EXCL_START
} else if (ferror(file)) {
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)",
name.c_str(),
length + 1
);
// LCOV_EXCL_STOP
}
}
return false;

View File

@@ -2,9 +2,11 @@
#include "backtrace.hpp"
#include <stdlib.h> // strtoul
#include <optional>
#include <stdint.h>
#include "platform.hpp" // strcasecmp
#include "util.hpp" // parseWholeNumber
Tracing tracing;
@@ -22,8 +24,10 @@ bool trace_ParseTraceDepth(char const *arg) {
tracing.loud = false;
return true;
} else {
char *endptr;
tracing.depth = strtoul(arg, &endptr, 0);
return arg[0] != '\0' && *endptr == '\0';
std::optional<uint64_t> depth = parseWholeNumber(arg);
if (depth) {
tracing.depth = *depth;
}
return depth.has_value();
}
}

View File

@@ -63,25 +63,9 @@ std::pair<WarningState, std::optional<uint32_t>> getInitialWarningState(std::str
return {state, std::nullopt};
}
// Is the rest of the string a decimal number?
// We want to avoid `strtoul`'s whitespace and sign handling, so we parse manually
// If the rest of the string is a decimal number, it's the parameter value
char const *ptr = flag.c_str() + equals + 1;
uint32_t param = 0;
bool overflowed = false;
for (; isDigit(*ptr); ++ptr) {
if (overflowed) {
continue;
}
uint32_t c = *ptr - '0';
if (param > (UINT32_MAX - c) / 10) {
overflowed = true;
param = UINT32_MAX;
continue;
}
param = param * 10 + c;
}
uint64_t param = parseNumber(ptr, BASE_10).value_or(0);
// If we reached the end of the string, truncate it at the '='
if (*ptr == '\0') {
@@ -92,5 +76,5 @@ std::pair<WarningState, std::optional<uint32_t>> getInitialWarningState(std::str
}
}
return {state, param};
return {state, param > UINT32_MAX ? UINT32_MAX : param};
}

View File

@@ -240,16 +240,17 @@ static void
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
auto errorTooLarge = [&name]() {
error("\"%s\" has more than 65536 banks", name); // LCOV_EXCL_LINE
};
static constexpr off_t NB_BANKS_LIMIT = 0x10000;
static_assert(NB_BANKS_LIMIT * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS");
if (input == output) {
if (fileSize >= 0x10000 * BANK_SIZE) {
error("\"%s\" has more than 65536 banks", name);
return;
if (fileSize >= NB_BANKS_LIMIT * BANK_SIZE) {
return errorTooLarge(); // LCOV_EXCL_LINE
}
// 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;
@@ -258,19 +259,13 @@ static void
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;
if (nbBanks == NB_BANKS_LIMIT) {
return errorTooLarge(); // LCOV_EXCL_LINE
}
++nbBanks;
// Update global checksum, too
for (uint16_t i = 0; i < bankLen; ++i) {
globalSum += romx[totalRomxLen + i];
@@ -341,6 +336,7 @@ static void
// Pipes have already read ROMX and updated globalSum, but not regular files
if (input == output) {
for (;;) {
uint8_t bank[BANK_SIZE];
ssize_t bankLen = readBytes(input, bank, sizeof(bank));
for (uint16_t i = 0; i < bankLen; ++i) {
@@ -432,8 +428,9 @@ static void
// LCOV_EXCL_STOP
}
}
uint8_t bank[BANK_SIZE];
memset(bank, options.padValue, sizeof(bank));
size_t len = (nbBanks - 1) * BANK_SIZE - totalRomxLen; // Don't count ROM0!
size_t len = (nbBanks - 1) * sizeof(bank) - totalRomxLen; // Don't count ROM0!
while (len) {
static_assert(sizeof(bank) <= SSIZE_MAX, "Bank too large for reading");
@@ -470,8 +467,10 @@ bool fix_ProcessFile(char const *name, char const *outputName) {
} else {
output = open(outputName, O_WRONLY | O_BINARY | O_CREAT, 0600);
if (output == -1) {
// LCOV_EXCL_START
error("Failed to open \"%s\" for writing: %s", outputName, strerror(errno));
return true;
// LCOV_EXCL_STOP
}
openedOutput = true;
}

View File

@@ -3,7 +3,9 @@
#include "fix/main.hpp"
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <optional>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
@@ -16,6 +18,7 @@
#include "platform.hpp"
#include "style.hpp"
#include "usage.hpp"
#include "util.hpp"
#include "version.hpp"
#include "fix/fix.hpp"
@@ -89,25 +92,13 @@ static Usage usage = {
// clang-format on
static void parseByte(uint16_t &output, char name) {
if (musl_optarg[0] == 0) {
fatal("Argument to option '-%c' may not be empty", name);
}
char *endptr;
unsigned long value;
if (musl_optarg[0] == '$') {
value = strtoul(&musl_optarg[1], &endptr, 16);
if (std::optional<uint64_t> value = parseWholeNumber(musl_optarg); !value) {
fatal("Invalid argument for option '-%c'", name);
} else if (*value > 0xFF) {
fatal("Argument for option '-%c' must be between 0 and 0xFF", name);
} else {
value = strtoul(musl_optarg, &endptr, 0);
output = *value;
}
if (*endptr) {
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);
}
output = value;
}
static uint8_t const nintendoLogo[] = {
@@ -122,12 +113,16 @@ static void initLogo() {
if (strcmp(options.logoFilename, "-")) {
logoFile = fopen(options.logoFilename, "rb");
} else {
// LCOV_EXCL_START
options.logoFilename = "<stdin>";
(void)setmode(STDIN_FILENO, O_BINARY);
logoFile = stdin;
// LCOV_EXCL_STOP
}
if (!logoFile) {
// LCOV_EXCL_START
fatal("Failed to open \"%s\" for reading: %s", options.logoFilename, strerror(errno));
// LCOV_EXCL_STOP
}
Defer closeLogo{[&] { fclose(logoFile); }};
@@ -330,8 +325,10 @@ int main(int argc, char *argv[]) {
}
break;
// LCOV_EXCL_START
default:
usage.printAndExit(1); // LCOV_EXCL_LINE
usage.printAndExit(1);
// LCOV_EXCL_STOP
}
}

View File

@@ -2,15 +2,17 @@
#include "fix/mbc.hpp"
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unordered_map>
#include <utility>
#include "helpers.hpp" // unreachable_
#include "platform.hpp" // strcasecmp
#include "util.hpp" // isBlankSpace, isDigit
#include "util.hpp" // isBlankSpace, isLower, isDigit
#include "fix/warning.hpp"
@@ -96,87 +98,59 @@ bool mbc_HasRAM(MbcType type) {
return search != mbcData.end() && search->second.second;
}
static void skipBlankSpace(char const *&ptr) {
while (isBlankSpace(*ptr)) {
++ptr;
}
}
static void skipMBCSpace(char const *&ptr) {
while (isBlankSpace(*ptr) || *ptr == '_') {
++ptr;
}
ptr += strspn(ptr, " \t_");
}
static char normalizeMBCChar(char c) {
if (c >= 'a' && c <= 'z') { // Uppercase for comparison with `mbc_Name`s
c = c - 'a' + 'A';
} else if (c == '_') { // Treat underscores as spaces
c = ' ';
if (isLower(c)) {
c = c - 'a' + 'A'; // Uppercase for comparison with `mbc_Name`s
} else if (c == '_') {
c = ' '; // Treat underscores as spaces
}
return c;
}
static bool readMBCSlice(char const *&name, char const *expected) {
while (*expected) {
// If `name` is too short, the character will be '\0' and this will return `false`
if (normalizeMBCChar(*name++) != *expected++) {
return false;
}
}
return true;
[[noreturn]]
static void fatalUnknownMBC(char const *name) {
fatal("Unknown MBC \"%s\"\n%s", name, acceptedMBCNames);
}
[[noreturn]]
static void fatalUnknownMBC(char const *fullName) {
fatal("Unknown MBC \"%s\"\n%s", fullName, acceptedMBCNames);
}
[[noreturn]]
static void fatalWrongMBCFeatures(char const *fullName) {
fatal("Features incompatible with MBC (\"%s\")\n%s", fullName, acceptedMBCNames);
static void fatalWrongMBCFeatures(char const *name) {
fatal("Features incompatible with MBC (\"%s\")\n%s", name, acceptedMBCNames);
}
MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor) {
char const *fullName = name;
char const *ptr = name + strspn(name, " \t"); // Skip leading blank space
if (!strcasecmp(name, "help") || !strcasecmp(name, "list")) {
if (!strcasecmp(ptr, "help") || !strcasecmp(ptr, "list")) {
puts(acceptedMBCNames); // Outputs to stdout and appends a newline
exit(0);
}
if (isDigit(name[0]) || name[0] == '$') {
int base = 0;
if (name[0] == '$') {
++name;
base = 16;
// Parse numeric MBC and return it as-is (unless it's too large)
if (char c = *ptr; isDigit(c) || c == '$' || c == '&' || c == '%') {
if (std::optional<uint64_t> mbc = parseWholeNumber(ptr); !mbc) {
fatalUnknownMBC(name);
} else if (*mbc > 0xFF) {
fatal("Specified MBC ID out of range 0-255: \"%s\"", name);
} else {
return static_cast<MbcType>(*mbc);
}
// Parse number, and return it as-is (unless it's too large)
char *endptr;
unsigned long mbc = strtoul(name, &endptr, base);
if (*endptr) {
fatalUnknownMBC(fullName);
}
if (mbc > 0xFF) {
fatal("Specified MBC ID out of range 0-255: \"%s\"", fullName);
}
return static_cast<MbcType>(mbc);
}
// Begin by reading the MBC type:
uint16_t mbc;
char const *ptr = name;
uint16_t mbc = UINT16_MAX;
skipBlankSpace(ptr); // Trim off leading blank space
#define tryReadSlice(expected) \
do { \
if (!readMBCSlice(ptr, expected)) { \
fatalUnknownMBC(fullName); \
} \
} while (0)
auto tryReadSlice = [&ptr, &name](char const *expected) {
while (*expected) {
// If `name` is too short, the character will be '\0' and this will return `false`
if (normalizeMBCChar(*ptr++) != *expected++) {
fatalUnknownMBC(name);
}
}
};
switch (*ptr++) {
case 'R': // ROM / ROM_ONLY
@@ -196,13 +170,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
switch (*ptr++) {
case 'B':
case 'b':
switch (*ptr++) {
case 'C':
case 'c':
break;
default:
fatalUnknownMBC(fullName);
}
tryReadSlice("C");
switch (*ptr++) {
case '1':
mbc = MBC1;
@@ -222,8 +190,6 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
case '7':
mbc = MBC7_SENSOR_RUMBLE_RAM_BATTERY;
break;
default:
fatalUnknownMBC(fullName);
}
break;
case 'M':
@@ -231,8 +197,6 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
tryReadSlice("M01");
mbc = MMM01;
break;
default:
fatalUnknownMBC(fullName);
}
break;
@@ -255,39 +219,30 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
tryReadSlice("MA5");
mbc = BANDAI_TAMA5;
break;
case 'P': {
case 'P':
tryReadSlice("P1");
// Parse version
skipMBCSpace(ptr);
// Major
char *endptr;
unsigned long val = strtoul(ptr, &endptr, 10);
if (endptr == ptr) {
if (std::optional<uint64_t> major = parseNumber(ptr, BASE_10); !major) {
fatal("Failed to parse TPP1 major revision number");
}
ptr = endptr;
if (val != 1) {
} else if (*major != 1) {
fatal("RGBFIX only supports TPP1 version 1.0");
} else {
tpp1Major = *major;
}
tpp1Major = val;
tryReadSlice(".");
// Minor
val = strtoul(ptr, &endptr, 10);
if (endptr == ptr) {
if (std::optional<uint64_t> minor = parseNumber(ptr, BASE_10); !minor) {
fatal("Failed to parse TPP1 minor revision number");
}
ptr = endptr;
if (val > 0xFF) {
} else if (*minor > 0xFF) {
fatal("TPP1 minor revision number must be 8-bit");
} else {
tpp1Minor = *minor;
}
tpp1Minor = val;
mbc = TPP1;
break;
}
default:
fatalUnknownMBC(fullName);
}
break;
case 'H': // HuC{1, 3}
@@ -300,13 +255,12 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
case '3':
mbc = HUC3;
break;
default:
fatalUnknownMBC(fullName);
}
break;
}
default:
fatalUnknownMBC(fullName);
if (mbc == UINT16_MAX) {
fatalUnknownMBC(name);
}
// Read "additional features"
@@ -320,18 +274,10 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
static constexpr uint8_t MULTIRUMBLE = 1 << 2;
// clang-format on
for (;;) {
skipBlankSpace(ptr); // Trim off trailing blank space
// If done, start processing "features"
if (!*ptr) {
break;
}
while (*ptr) {
// We expect a '+' at this point
skipMBCSpace(ptr);
if (*ptr++ != '+') {
fatalUnknownMBC(fullName);
}
tryReadSlice("+");
skipMBCSpace(ptr);
switch (*ptr++) {
@@ -360,8 +306,6 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
tryReadSlice("M");
features |= RAM;
break;
default:
fatalUnknownMBC(fullName);
}
break;
@@ -376,12 +320,8 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
tryReadSlice("IMER");
features |= TIMER;
break;
default:
fatalUnknownMBC(fullName);
}
}
#undef tryReadSlice
switch (mbc) {
case ROM:
@@ -402,7 +342,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
} else if (features == (RAM | BATTERY)) {
mbc += 2;
} else if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
@@ -410,7 +350,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
if (features == BATTERY) {
mbc = MBC2_BATTERY;
} else if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
@@ -434,7 +374,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
} else if (features == (RAM | BATTERY)) {
mbc += 2;
} else if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
@@ -452,7 +392,7 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
} else if (features == (RAM | BATTERY)) {
mbc += 2;
} else if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
@@ -462,53 +402,50 @@ MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor)
case HUC3:
// No extra features accepted
if (features) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
if (features != (SENSOR | RUMBLE | RAM | BATTERY)) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
case HUC1_RAM_BATTERY:
if (features != (RAM | BATTERY)) { // HuC1 expects RAM+BATTERY
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
break;
case TPP1:
case TPP1: {
// clang-format off: vertically align values
static constexpr uint8_t BATTERY_TPP1 = 1 << 3;
static constexpr uint8_t TIMER_TPP1 = 1 << 2;
static constexpr uint8_t MULTIRUMBLE_TPP1 = 1 << 1;
static constexpr uint8_t RUMBLE_TPP1 = 1 << 0;
// clang-format on
if (features & RAM) {
warning(WARNING_MBC, "TPP1 requests RAM implicitly if given a non-zero RAM size");
}
if (features & BATTERY) {
mbc |= 0x08;
mbc |= BATTERY_TPP1;
}
if (features & TIMER) {
mbc |= 0x04;
}
if (features & MULTIRUMBLE) {
mbc |= 0x03; // Also set the rumble flag
mbc |= TIMER_TPP1;
}
if (features & RUMBLE) {
mbc |= 0x01;
mbc |= RUMBLE_TPP1;
}
if (features & SENSOR) {
fatalWrongMBCFeatures(fullName);
fatalWrongMBCFeatures(name);
}
// Multiple rumble speeds imply rumble
if (mbc & 0x01) {
assume(mbc & 0x02);
if (features & MULTIRUMBLE) {
mbc |= MULTIRUMBLE_TPP1 | RUMBLE_TPP1; // Multiple rumble speeds imply rumble
}
break;
}
skipBlankSpace(ptr); // Trim off trailing blank space
// If there is still something left, error out
if (*ptr) {
fatalUnknownMBC(fullName);
}
return static_cast<MbcType>(mbc);

View File

@@ -3,10 +3,10 @@
#include "gfx/main.hpp"
#include <algorithm>
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <ios>
#include <optional>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
@@ -44,6 +44,8 @@ static struct LocalOptions {
bool autoPalmap;
bool groupOutputs;
bool reverse;
bool autoAny() const { return autoAttrmap || autoTilemap || autoPalettes || autoPalmap; }
} localOptions;
// Short options
@@ -120,76 +122,19 @@ static Usage usage = {
// Parses a number at the beginning of a string, moving the pointer to skip the parsed characters.
// Returns the provided errVal on error.
static uint16_t parseNumber(char *&string, char const *errPrefix, uint16_t errVal = UINT16_MAX) {
uint8_t base = 10;
if (*string == '\0') {
static uint16_t readNumber(char const *&str, char const *errPrefix, uint16_t errVal = UINT16_MAX) {
if (std::optional<uint64_t> number = parseNumber(str); !number) {
error("%s: expected number, but found nothing", errPrefix);
return errVal;
} else if (*string == '$') {
base = 16;
++string;
} else if (*string == '%') {
base = 2;
++string;
} else if (*string == '0' && string[1] != '\0') {
// Check if we have a "0x" or "0b" here
if (string[1] == 'x' || string[1] == 'X') {
base = 16;
string += 2;
} else if (string[1] == 'b' || string[1] == 'B') {
base = 2;
string += 2;
}
}
// Turns a digit into its numeric value in the current base, if it has one.
// Maximum is inclusive. The string_view is modified to "consume" all digits.
// Returns 255 on parse failure (including wrong char for base), in which case
// the string_view may be pointing on garbage.
auto charIndex = [&base](unsigned char c) -> uint8_t {
unsigned char index = c - '0'; // Use wrapping semantics
if (base == 2 && index >= 2) {
return 255;
} else if (index < 10) {
return index;
} else if (base != 16) {
return 255; // Letters are only valid in hex
}
index = tolower(c) - 'a'; // OK because we pass an `unsigned char`
if (index < 6) {
return index + 10;
}
return 255;
};
if (charIndex(*string) == 255) {
error(
"%s: expected digit%s, but found nothing", errPrefix, base != 10 ? " after base" : ""
);
return errVal;
}
uint16_t number = 0;
do {
// Read a character, and check if it's valid in the given base
uint8_t index = charIndex(*string);
if (index == 255) {
break; // Found an invalid character, end
}
++string;
number *= base;
number += index;
// The lax check covers the addition on top of the multiplication
if (number >= UINT16_MAX / base) {
} else if (*number > UINT16_MAX) {
error("%s: the number is too large!", errPrefix);
return errVal;
} else {
return *number;
}
} while (*string != '\0'); // No more characters?
return number;
}
static void skipBlankSpace(char *&arg) {
static void skipBlankSpace(char const *&arg) {
arg += strspn(arg, " \t");
}
@@ -261,7 +206,7 @@ static std::vector<size_t> readAtFile(std::string const &path, std::vector<char>
// to an "at-file" path if one is encountered.
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
char const *arg = musl_optarg; // Make a copy for scanning
switch (ch) {
case 'A':
@@ -281,7 +226,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'b': {
uint16_t number = parseNumber(arg, "Bank 0 base tile ID", 0);
uint16_t number = readNumber(arg, "Bank 0 base tile ID", 0);
if (number >= 256) {
error("Bank 0 base tile ID must be below 256");
} else {
@@ -301,7 +246,7 @@ static char *parseArgv(int argc, char *argv[]) {
}
++arg; // Skip comma
skipBlankSpace(arg);
number = parseNumber(arg, "Bank 1 base tile ID", 0);
number = readNumber(arg, "Bank 1 base tile ID", 0);
if (number >= 256) {
error("Bank 1 base tile ID must be below 256");
} else {
@@ -344,7 +289,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'd':
options.bitDepth = parseNumber(arg, "Bit depth", 2);
options.bitDepth = readNumber(arg, "Bit depth", 2);
if (*arg != '\0') {
error("Bit depth ('-b') argument must be a valid number, not \"%s\"", musl_optarg);
} else if (options.bitDepth != 1 && options.bitDepth != 2) {
@@ -366,7 +311,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'L':
options.inputSlice.left = parseNumber(arg, "Input slice left coordinate");
options.inputSlice.left = readNumber(arg, "Input slice left coordinate");
if (options.inputSlice.left > INT16_MAX) {
error("Input slice left coordinate is out of range!");
break;
@@ -378,7 +323,7 @@ static char *parseArgv(int argc, char *argv[]) {
}
++arg;
skipBlankSpace(arg);
options.inputSlice.top = parseNumber(arg, "Input slice upper coordinate");
options.inputSlice.top = readNumber(arg, "Input slice upper coordinate");
skipBlankSpace(arg);
if (*arg != ':') {
error("Missing colon after upper coordinate in \"%s\"", musl_optarg);
@@ -386,7 +331,7 @@ static char *parseArgv(int argc, char *argv[]) {
}
++arg;
skipBlankSpace(arg);
options.inputSlice.width = parseNumber(arg, "Input slice width");
options.inputSlice.width = readNumber(arg, "Input slice width");
skipBlankSpace(arg);
if (options.inputSlice.width == 0) {
error("Input slice width may not be 0!");
@@ -397,7 +342,7 @@ static char *parseArgv(int argc, char *argv[]) {
}
++arg;
skipBlankSpace(arg);
options.inputSlice.height = parseNumber(arg, "Input slice height");
options.inputSlice.height = readNumber(arg, "Input slice height");
if (options.inputSlice.height == 0) {
error("Input slice height may not be 0!");
}
@@ -407,7 +352,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'l': {
uint16_t number = parseNumber(arg, "Base palette ID", 0);
uint16_t number = readNumber(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) {
@@ -428,7 +373,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'N':
options.maxNbTiles[0] = parseNumber(arg, "Number of tiles in bank 0", 256);
options.maxNbTiles[0] = readNumber(arg, "Number of tiles in bank 0", 256);
if (options.maxNbTiles[0] > 256) {
error("Bank 0 cannot contain more than 256 tiles");
}
@@ -446,7 +391,7 @@ static char *parseArgv(int argc, char *argv[]) {
}
++arg; // Skip comma
skipBlankSpace(arg);
options.maxNbTiles[1] = parseNumber(arg, "Number of tiles in bank 1", 256);
options.maxNbTiles[1] = readNumber(arg, "Number of tiles in bank 1", 256);
if (options.maxNbTiles[1] > 256) {
error("Bank 1 cannot contain more than 256 tiles");
}
@@ -460,7 +405,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'n': {
uint16_t number = parseNumber(arg, "Number of palettes", 256);
uint16_t number = readNumber(arg, "Number of palettes", 256);
if (*arg != '\0') {
error("Number of palettes ('-n') must be a valid number, not \"%s\"", musl_optarg);
}
@@ -511,7 +456,7 @@ static char *parseArgv(int argc, char *argv[]) {
case 'r':
localOptions.reverse = true;
options.reversedWidth = parseNumber(arg, "Reversed image stride");
options.reversedWidth = readNumber(arg, "Reversed image stride");
if (*arg != '\0') {
error(
"Reversed image stride ('-r') must be a valid number, not \"%s\"", musl_optarg
@@ -520,7 +465,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 's':
options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4);
options.nbColorsPerPal = readNumber(arg, "Number of colors per palette", 4);
if (*arg != '\0') {
error("Palette size ('-s') must be a valid number, not \"%s\"", musl_optarg);
}
@@ -562,7 +507,7 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'x':
options.trim = parseNumber(arg, "Number of tiles to trim", 0);
options.trim = readNumber(arg, "Number of tiles to trim", 0);
if (*arg != '\0') {
error("Tile trim ('-x') argument must be a valid number, not \"%s\"", musl_optarg);
}
@@ -597,8 +542,10 @@ static char *parseArgv(int argc, char *argv[]) {
}
break;
// LCOV_EXCL_START
default:
usage.printAndExit(1); // LCOV_EXCL_LINE
usage.printAndExit(1);
// LCOV_EXCL_STOP
}
}
@@ -685,7 +632,8 @@ static void verboseOutputConfig() {
fputs("\t]\n", stderr);
}
// -L/--slice
if (options.inputSlice.specified()) {
if (options.inputSlice.width || options.inputSlice.height || options.inputSlice.left
|| options.inputSlice.top) {
fprintf(
stderr,
"\tInput image slice: %" PRIu16 "x%" PRIu16 " pixels starting at (%" PRIu16 ", %" PRIu16
@@ -846,6 +794,16 @@ int main(int argc, char *argv[]) {
);
}
if (localOptions.groupOutputs) {
if (!localOptions.autoAny()) {
warnx("Grouping outputs ('-O') is enabled, but without any automatic output paths "
"('-A', '-P', '-Q', or '-T')");
}
if (options.output.empty()) {
warnx("Grouping outputs ('-O') is enabled, but without an output tile data file ('-o')"
);
}
}
auto autoOutPath = [](bool autoOptEnabled, std::string &path, char const *extension) {
if (!autoOptEnabled) {
return;
@@ -918,21 +876,17 @@ auto Palette::begin() -> decltype(colors)::iterator {
auto Palette::end() -> decltype(colors)::iterator {
// Return an iterator pointing past the last non-empty element.
// Since the palette may contain gaps, we must scan from the end.
return std::find_if(
colors.rbegin(), colors.rend(), [](uint16_t c) { return c != UINT16_MAX; }
).base();
return std::find_if(RRANGE(colors), [](uint16_t c) { return c != UINT16_MAX; }).base();
}
auto Palette::begin() const -> decltype(colors)::const_iterator {
// Skip the first slot if reserved for transparency
// Same as the non-const begin().
return colors.begin() + options.hasTransparentPixels;
}
auto Palette::end() const -> decltype(colors)::const_iterator {
// Same as the non-const end().
return std::find_if(
colors.rbegin(), colors.rend(), [](uint16_t c) { return c != UINT16_MAX; }
).base();
return std::find_if(RRANGE(colors), [](uint16_t c) { return c != UINT16_MAX; }).base();
}
uint8_t Palette::size() const {

View File

@@ -9,6 +9,7 @@
#include <numeric>
#include <optional>
#include <queue>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <type_traits>
@@ -42,9 +43,11 @@ struct ColorSetAttrs {
std::vector<bool> bannedPages;
explicit ColorSetAttrs(size_t index) : colorSetIndex(index) {}
bool isBannedFrom(size_t index) const {
return index < bannedPages.size() && bannedPages[index];
}
void banFrom(size_t index) {
if (bannedPages.size() <= index) {
bannedPages.resize(index + 1);
@@ -62,28 +65,31 @@ class AssignedSets {
std::vector<ColorSet> const *_colorSets;
public:
template<typename... Ts>
AssignedSets(std::vector<ColorSet> const &colorSets, Ts &&...elems)
: _assigned{std::forward<Ts>(elems)...}, _colorSets{&colorSets} {}
AssignedSets(std::vector<ColorSet> const &colorSets, std::optional<ColorSetAttrs> &&attrs)
: _assigned{attrs}, _colorSets{&colorSets} {}
private:
template<typename Inner, template<typename> typename Constness>
class Iter {
// Template class for both const and non-const iterators over the non-empty `_assigned` slots
template<typename I, template<typename> typename Constness>
class AssignedSetsIter {
public:
friend class AssignedSets;
// For `iterator_traits`
using difference_type = typename std::iterator_traits<Inner>::difference_type;
using value_type = ColorSetAttrs;
using pointer = Constness<value_type> *;
using difference_type = ptrdiff_t;
using reference = Constness<value_type> &;
using pointer = Constness<value_type> *;
using iterator_category = std::forward_iterator_tag;
private:
Constness<decltype(_assigned)> *_array = nullptr;
Inner _iter{};
I _iter{};
Iter(decltype(_array) array, decltype(_iter) &&iter) : _array(array), _iter(iter) {}
Iter &skipEmpty() {
AssignedSetsIter(decltype(_array) array, decltype(_iter) &&iter)
: _array(array), _iter(iter) {}
AssignedSetsIter &skipEmpty() {
while (_iter != _array->end() && !_iter->has_value()) {
++_iter;
}
@@ -91,17 +97,17 @@ private:
}
public:
Iter() = default;
AssignedSetsIter() = default;
bool operator==(Iter const &rhs) const { return _iter == rhs._iter; }
bool operator==(AssignedSetsIter const &rhs) const { return _iter == rhs._iter; }
Iter &operator++() {
AssignedSetsIter &operator++() {
++_iter;
skipEmpty();
return *this;
}
Iter operator++(int) {
Iter it = *this;
AssignedSetsIter operator++(int) {
AssignedSetsIter it = *this;
++(*this);
return it;
}
@@ -114,56 +120,55 @@ private:
return &(**this); // Invokes the operator above, not quite a no-op!
}
friend void swap(Iter &lhs, Iter &rhs) {
friend void swap(AssignedSetsIter &lhs, AssignedSetsIter &rhs) {
std::swap(lhs._array, rhs._array);
std::swap(lhs._iter, rhs._iter);
}
};
public:
using iterator = Iter<decltype(_assigned)::iterator, std::remove_const_t>;
using iterator = AssignedSetsIter<decltype(_assigned)::iterator, std::remove_const_t>;
iterator begin() { return iterator{&_assigned, _assigned.begin()}.skipEmpty(); }
iterator end() { return iterator{&_assigned, _assigned.end()}; }
using const_iterator = Iter<decltype(_assigned)::const_iterator, std::add_const_t>;
using const_iterator = AssignedSetsIter<decltype(_assigned)::const_iterator, std::add_const_t>;
const_iterator begin() const {
return const_iterator{&_assigned, _assigned.begin()}.skipEmpty();
}
const_iterator end() const { return const_iterator{&_assigned, _assigned.end()}; }
// Assigns a new ColorSetAttrs in a free slot, assuming there is one
// Args are passed to the `ColorSetAttrs`'s constructor
template<typename... Ts>
void assign(Ts &&...args) {
void assign(ColorSetAttrs const &&attrs) {
auto freeSlot =
std::find_if_not(RANGE(_assigned), [](std::optional<ColorSetAttrs> const &slot) {
return slot.has_value();
});
if (freeSlot == _assigned.end()) {
_assigned.emplace_back(attrs); // We are full, use a new slot
} else {
freeSlot->emplace(attrs); // Reuse a free slot
}
}
if (freeSlot == _assigned.end()) { // We are full, use a new slot
_assigned.emplace_back(std::forward<Ts>(args)...);
} else { // Reuse a free slot
freeSlot->emplace(std::forward<Ts>(args)...);
}
}
void remove(iterator const &iter) {
iter._iter->reset(); // This time, we want to access the `optional` itself
}
void clear() { _assigned.clear(); }
bool empty() const {
return std::find_if(
RANGE(_assigned),
[](std::optional<ColorSetAttrs> const &slot) { return slot.has_value(); }
)
== _assigned.end();
return std::none_of(RANGE(_assigned), [](std::optional<ColorSetAttrs> const &slot) {
return slot.has_value();
});
}
size_t nbColorSets() const { return std::distance(RANGE(*this)); }
private:
template<typename Iter>
template<typename I>
static void addUniqueColors(
std::unordered_set<uint16_t> &colors,
Iter iter,
Iter const &end,
I iter,
I const &end,
std::vector<ColorSet> const &colorSets
) {
for (; iter != end; ++iter) {
@@ -171,6 +176,7 @@ private:
colors.insert(RANGE(colorSet));
}
}
// This function should stay private because it returns a reference to a unique object
std::unordered_set<uint16_t> &uniqueColors() const {
// We check for *distinct* colors by stuffing them into a `set`; this should be
@@ -181,9 +187,11 @@ private:
addUniqueColors(colors, RANGE(*this), *_colorSets);
return colors;
}
public:
// Returns the number of distinct colors
size_t volume() const { return uniqueColors().size(); }
bool canFit(ColorSet const &colorSet) const {
std::unordered_set<uint16_t> &colors = uniqueColors();
colors.insert(RANGE(colorSet));
@@ -232,18 +240,18 @@ public:
}
// Computes the "relative size" of a set of color sets on this palette
template<typename Iter>
size_t combinedVolume(Iter &&begin, Iter const &end, std::vector<ColorSet> const &colorSets)
const {
template<typename I>
size_t combinedVolume(I &&begin, I const &end, std::vector<ColorSet> const &colorSets) const {
std::unordered_set<uint16_t> &colors = uniqueColors();
addUniqueColors(colors, std::forward<Iter>(begin), end, colorSets);
addUniqueColors(colors, std::forward<I>(begin), end, colorSets);
return colors.size();
}
// Computes the "relative size" of a set of colors on this palette
template<typename Iter>
size_t combinedVolume(Iter &&begin, Iter &&end) const {
template<typename I>
size_t combinedVolume(I &&begin, I &&end) const {
std::unordered_set<uint16_t> &colors = uniqueColors();
colors.insert(std::forward<Iter>(begin), std::forward<Iter>(end));
colors.insert(std::forward<I>(begin), std::forward<I>(end));
return colors.size();
}
};
@@ -302,7 +310,7 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
// If the entire palettes can be merged, move all of `from`'s color sets
if (to.combinedVolume(RANGE(from), colorSets) <= options.maxOpaqueColors()) {
for (ColorSetAttrs &attrs : from) {
to.assign(attrs.colorSetIndex);
to.assign(std::move(attrs));
}
from.clear();
}
@@ -316,23 +324,23 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
// We do this by adding the first available color set, and then looking for palettes with
// common colors. (As an optimization, we know we can skip palettes already scanned.)
std::vector<bool> processed(from.nbColorSets(), false);
for (std::vector<bool>::iterator iter;
(iter = std::find(RANGE(processed), false)) != processed.end();) {
for (std::vector<bool>::iterator wasProcessed;
(wasProcessed = std::find(RANGE(processed), false)) != processed.end();) {
auto attrs = from.begin();
std::advance(attrs, iter - processed.begin());
std::advance(attrs, wasProcessed - processed.begin());
std::unordered_set<uint16_t> colors(RANGE(colorSets[attrs->colorSetIndex]));
std::vector<size_t> members = {static_cast<size_t>(iter - processed.begin())};
*iter = true; // Mark the first color set as processed
std::vector<size_t> members = {static_cast<size_t>(wasProcessed - processed.begin())};
*wasProcessed = true; // Mark the first color set as processed
// Build up the "component"...
for (; ++iter != processed.end(); ++attrs) {
for (; ++wasProcessed != processed.end(); ++attrs) {
// If at least one color matches, add it
if (ColorSet const &colorSet = colorSets[attrs->colorSetIndex];
std::find_first_of(RANGE(colors), RANGE(colorSet)) != colors.end()) {
colors.insert(RANGE(colorSet));
members.push_back(iter - processed.begin());
*iter = true; // Mark that color set as processed
members.push_back(wasProcessed - processed.begin());
*wasProcessed = true; // Mark that color set as processed
}
}
@@ -357,10 +365,10 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
// Decant on individual color sets
decantOn([&colorSets](AssignedSets &to, AssignedSets &from) {
for (auto iter = from.begin(); iter != from.end(); ++iter) {
if (to.canFit(colorSets[iter->colorSetIndex])) {
to.assign(std::move(*iter));
from.remove(iter);
for (auto it = from.begin(); it != from.end(); ++it) {
if (to.canFit(colorSets[it->colorSetIndex])) {
to.assign(std::move(*it));
from.remove(it);
}
}
});
@@ -525,10 +533,10 @@ std::pair<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet> c
// Place back any color sets now in the queue via first-fit
for (ColorSetAttrs const &attrs : overloadQueue) {
ColorSet const &colorSet = colorSets[attrs.colorSetIndex];
auto iter = std::find_if(RANGE(assignments), [&colorSet](AssignedSets const &pal) {
auto palette = std::find_if(RANGE(assignments), [&colorSet](AssignedSets const &pal) {
return pal.canFit(colorSet);
});
if (iter == assignments.end()) { // No such page, create a new one
if (palette == assignments.end()) { // No such page, create a new one
verbosePrint(
VERB_DEBUG,
"Adding new palette (%zu) for overflowing color set %zu\n",
@@ -541,9 +549,9 @@ std::pair<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet> c
VERB_DEBUG,
"Assigning overflowing color set %zu to palette %zu\n",
attrs.colorSetIndex,
iter - assignments.begin()
palette - assignments.begin()
);
iter->assign(std::move(attrs));
palette->assign(std::move(attrs));
}
}

View File

@@ -32,32 +32,22 @@ using namespace std::string_view_literals;
static char const *hexDigits = "0123456789ABCDEFabcdef";
template<typename Str> // Should be std::string or std::string_view
static void skipBlankSpace(Str const &str, size_t &pos) {
static void skipBlankSpace(std::string_view const &str, size_t &pos) {
pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length());
}
static constexpr uint8_t nibble(char c) {
if (c >= 'a') {
assume(c <= 'f');
return c - 'a' + 10;
} else if (c >= 'A') {
assume(c <= 'F');
return c - 'A' + 10;
} else {
assume(isDigit(c));
return c - '0';
}
static uint8_t toHex(char c1, char c2) {
return parseHexDigit(c1) * 16 + parseHexDigit(c2);
}
static constexpr uint8_t toHex(char c1, char c2) {
return nibble(c1) * 16 + nibble(c2);
}
static constexpr uint8_t singleToHex(char c) {
static uint8_t singleToHex(char c) {
return toHex(c, c);
}
static uint16_t toWord(uint8_t low, uint8_t high) {
return high << 8 | low;
}
void parseInlinePalSpec(char const * const rawArg) {
// List of #rrggbb/#rgb colors (or #none); comma-separated.
// Palettes are separated by colons.
@@ -176,24 +166,6 @@ void parseInlinePalSpec(char const * const rawArg) {
}
}
template<typename T, typename U>
static T readBE(U const *bytes) {
T val = 0;
for (size_t i = 0; i < sizeof(val); ++i) {
val = val << 8 | static_cast<uint8_t>(bytes[i]);
}
return val;
}
template<typename T, typename U>
static T readLE(U const *bytes) {
T val = 0;
for (size_t i = 0; i < sizeof(val); ++i) {
val |= static_cast<uint8_t>(bytes[i]) << (i * 8);
}
return val;
}
// Appends the first line read from `file` to the end of the provided `buffer`.
// Returns true if a line was read.
[[gnu::warn_unused_result]]
@@ -436,7 +408,7 @@ static void parseACTFile(char const *filename, std::filebuf &file) {
uint16_t nbColors = 256;
if (len == 772) {
nbColors = readBE<uint16_t>(&buf[768]);
nbColors = toWord(buf[769], buf[768]);
if (nbColors > 256 || nbColors == 0) {
error("Invalid number of colors in ACT file \"%s\" (%" PRIu16 ")", filename, nbColors);
return;
@@ -487,7 +459,7 @@ static void parseACOFile(char const *filename, std::filebuf &file) {
error("Failed to read ACO file version");
return;
}
if (readBE<uint16_t>(buf) != 1) {
if (toWord(buf[1], buf[0]) != 1) {
error("File \"%s\" is not a valid ACO v1 file", filename);
return;
}
@@ -496,7 +468,7 @@ static void parseACOFile(char const *filename, std::filebuf &file) {
error("Failed to read number of colors in palette file");
return;
}
uint16_t nbColors = readBE<uint16_t>(buf);
uint16_t nbColors = toWord(buf[1], buf[0]);
if (uint16_t maxNbColors = options.maxNbColors(); nbColors > maxNbColors) {
warnExtraColors("ACO", filename, nbColors, maxNbColors);
@@ -516,7 +488,7 @@ static void parseACOFile(char const *filename, std::filebuf &file) {
}
std::optional<Rgba> &color = options.palSpec.back()[i % options.nbColorsPerPal];
uint16_t colorType = readBE<uint16_t>(buf);
uint16_t colorType = toWord(buf[1], buf[0]);
switch (colorType) {
case 0: // RGB
// Only keep the MSB of the (big-endian) 16-bit values.
@@ -562,10 +534,10 @@ static void parseGBCFile(char const *filename, std::filebuf &file) {
}
options.palSpec.push_back({
Rgba::fromCGBColor(readLE<uint16_t>(&buf[0])),
Rgba::fromCGBColor(readLE<uint16_t>(&buf[2])),
Rgba::fromCGBColor(readLE<uint16_t>(&buf[4])),
Rgba::fromCGBColor(readLE<uint16_t>(&buf[6])),
Rgba::fromCGBColor(toWord(buf[0], buf[1])),
Rgba::fromCGBColor(toWord(buf[2], buf[3])),
Rgba::fromCGBColor(toWord(buf[4], buf[5])),
Rgba::fromCGBColor(toWord(buf[6], buf[7])),
});
}
}

View File

@@ -235,7 +235,7 @@ void reverse() {
palette.begin() + options.nbColorsPerPal,
[&buf, i = 0]() mutable {
i += 2;
return Rgba::fromCGBColor(buf[i - 2] + (buf[i - 1] << 8));
return Rgba::fromCGBColor(buf[i - 2] | buf[i - 1] << 8); // little-endian
}
);
}

View File

@@ -50,9 +50,10 @@ uint16_t Rgba::cgbColor() const {
g = reverse_curve[g];
b = reverse_curve[b];
} else {
r >>= 3;
g >>= 3;
b >>= 3;
constexpr auto _8to5 = [](uint8_t c) -> uint8_t { return (c * 31 + 127) / 255; };
r = _8to5(r);
g = _8to5(g);
b = _8to5(b);
}
return r | g << 5 | b << 10;
}

View File

@@ -101,7 +101,7 @@ void warning(WarningID id, char const *fmt, ...) {
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
style_Set(stderr, STYLE_YELLOW, true);
style_Set(stderr, STYLE_RED, true);
fprintf(stderr, " [-Werror=%s]\n", flag);
style_Reset(stderr);

View File

@@ -53,11 +53,10 @@ static void initFreeSpace() {
static void assignSection(Section &section, MemoryLocation const &location) {
// Propagate the assigned location to all UNIONs/FRAGMENTs
// so `jr` patches in them will have the correct offset
for (Section *next = &section; next != nullptr; next = next->nextu.get()) {
next->org = location.address;
next->bank = location.bank;
for (Section *piece = &section; piece != nullptr; piece = piece->nextPiece.get()) {
piece->org = location.address;
piece->bank = location.bank;
}
out_AddSection(section);
}

View File

@@ -128,6 +128,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
layout_SetAddr(floatingAlignOffset);
} else {
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
if (alignOfs >= alignSize) {
scriptError(
@@ -139,8 +140,8 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
return;
}
floatingAlignMask = alignSize - 1;
floatingAlignOffset = alignOfs % alignSize;
floatingAlignMask = alignMask;
floatingAlignOffset = alignOfs & alignMask;
}
return;
}
@@ -158,6 +159,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
if (alignment < 16) {
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
if (alignOfs >= alignSize) {
scriptError(
@@ -170,7 +172,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
}
assume(pc >= typeInfo.startAddr);
length %= alignSize;
length &= alignMask;
}
if (uint16_t offset = pc - typeInfo.startAddr; length > typeInfo.size - offset) {
@@ -248,10 +250,9 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
typeInfo.name.c_str()
);
} else {
// SDCC areas don't have a type assigned yet, so the linker script is used to give them
// one.
for (Section *fragment = section; fragment; fragment = fragment->nextu.get()) {
fragment->type = activeType;
// SDCC areas don't have a type assigned yet, so the linker script gives them one.
for (Section *piece = section; piece != nullptr; piece = piece->nextPiece.get()) {
piece->type = activeType;
}
}
} else if (section->type != activeType) {

View File

@@ -13,7 +13,6 @@
#include <vector>
#include "backtrace.hpp"
#include "helpers.hpp"
#include "linkdefs.hpp"
#include "util.hpp"
@@ -107,10 +106,6 @@ static yy::parser::symbol_type parseDecNumber(int c) {
return yy::parser::make_number(number);
}
static bool isBinDigit(int c) {
return c >= '0' && c <= '1';
}
static yy::parser::symbol_type parseBinNumber(char const *prefix) {
LexerStackEntry &context = lexerStack.back();
int c = context.file.sgetc();
@@ -149,18 +144,6 @@ static yy::parser::symbol_type parseOctNumber(char const *prefix) {
return yy::parser::make_number(number);
}
static uint8_t parseHexDigit(int c) {
if (isDigit(c)) {
return c - '0';
} else if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
} else if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
} else {
unreachable_(); // LCOV_EXCL_LINE
}
}
static yy::parser::symbol_type parseHexNumber(char const *prefix) {
LexerStackEntry &context = lexerStack.back();
int c = context.file.sgetc();
@@ -180,7 +163,7 @@ static yy::parser::symbol_type parseHexNumber(char const *prefix) {
return yy::parser::make_number(number);
}
static yy::parser::symbol_type parseNumber(int c) {
static yy::parser::symbol_type parseAnyNumber(int c) {
LexerStackEntry &context = lexerStack.back();
if (c == '0') {
switch (context.file.sgetc()) {
@@ -278,7 +261,7 @@ yy::parser::symbol_type yylex() {
} else if (c == '&') {
return parseOctNumber("'&'");
} else if (isDigit(c)) {
return parseNumber(c);
return parseAnyNumber(c);
} else if (isLetter(c)) {
std::string keyword = readKeyword(c);

View File

@@ -4,6 +4,7 @@
#include <inttypes.h>
#include <limits.h>
#include <optional>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
@@ -265,21 +266,18 @@ static void parseScrambleSpec(char *spec) {
uint16_t limit = search->second.second;
if (regionSize) {
char *endptr;
unsigned long value = strtoul(regionSize, &endptr, 0);
if (*endptr != '\0') {
char const *ptr = regionSize + skipBlankSpace(regionSize);
if (std::optional<uint64_t> value = parseWholeNumber(ptr); !value) {
fatal("Invalid region size limit \"%s\" for option '-S'", regionSize);
}
if (value > limit) {
} else if (*value > limit) {
fatal(
"%s region size for option '-S' must be between 0 and %" PRIu16,
search->first.c_str(),
limit
);
} else {
limit = *value;
}
limit = value;
} else if (search->second.first != &options.scrambleWRAMX) {
// Only WRAMX limit can be implied, since ROMX and SRAM size may vary.
fatal("Missing %s region size limit for option '-S'", search->first.c_str());
@@ -353,21 +351,16 @@ int main(int argc, char *argv[]) {
options.outputFileName = musl_optarg;
break;
case 'p': {
char *endptr;
unsigned long value = strtoul(musl_optarg, &endptr, 0);
if (musl_optarg[0] == '\0' || *endptr != '\0') {
case 'p':
if (std::optional<uint64_t> value = parseWholeNumber(musl_optarg); !value) {
fatal("Invalid argument for option '-p'");
}
if (value > 0xFF) {
} else if (*value > 0xFF) {
fatal("Argument for option '-p' must be between 0 and 0xFF");
}
options.padValue = value;
} else {
options.padValue = *value;
options.hasPadValue = true;
break;
}
break;
case 'S':
parseScrambleSpec(musl_optarg);
@@ -407,8 +400,10 @@ int main(int argc, char *argv[]) {
}
break;
// LCOV_EXCL_START
default:
usage.printAndExit(1); // LCOV_EXCL_LINE
usage.printAndExit(1);
// LCOV_EXCL_STOP
}
}
@@ -442,10 +437,10 @@ int main(int argc, char *argv[]) {
verbosePrint(VERB_NOTICE, "Reading linker script...\n");
if (lexer_Init(linkerScriptName)) {
yy::parser parser;
// We don't care about the return value, as any error increments the global error count,
// which is what `main` checks.
(void)parser.parse();
if (yy::parser parser; parser.parse() != 0) {
// Exited due to YYABORT or YYNOMEM
fatal("Unrecoverable error while reading linker script"); // LCOV_EXCL_LINE
}
}
// If the linker script produced any errors, some sections may be in an invalid state

View File

@@ -390,25 +390,6 @@ static void readSection(
}
}
// Links a symbol to a section, keeping the section's symbol list sorted.
static void linkSymToSect(Symbol &symbol, Section &section) {
uint32_t a = 0, b = section.symbols.size();
int32_t symbolOffset = symbol.label().offset;
while (a != b) {
uint32_t c = (a + b) / 2;
int32_t otherOffset = section.symbols[c]->label().offset;
if (otherOffset > symbolOffset) {
b = c;
} else {
a = c + 1;
}
}
section.symbols.insert(section.symbols.begin() + a, &symbol);
}
// Reads an assertion from a file.
static void readAssertion(
FILE *file,
@@ -512,15 +493,11 @@ void obj_ReadFile(char const *fileName, unsigned int fileID) {
std::vector<uint32_t> nbSymPerSect(nbSections, 0);
verbosePrint(VERB_INFO, "Reading %" PRIu32 " symbols...\n", nbSymbols);
for (uint32_t i = 0; i < nbSymbols; ++i) {
// Read symbol
Symbol &symbol = fileSymbols[i];
readSymbol(file, symbol, fileName, nodes[fileID]);
sym_AddSymbol(symbol);
if (std::holds_alternative<Label>(symbol.data)) {
++nbSymPerSect[std::get<Label>(symbol.data).sectionID];
for (Symbol &sym : fileSymbols) {
readSymbol(file, sym, fileName, nodes[fileID]);
sym_AddSymbol(sym);
if (std::holds_alternative<Label>(sym.data)) {
++nbSymPerSect[std::get<Label>(sym.data).sectionID];
}
}
@@ -529,9 +506,8 @@ void obj_ReadFile(char const *fileName, unsigned int fileID) {
verbosePrint(VERB_INFO, "Reading %" PRIu32 " sections...\n", nbSections);
for (uint32_t i = 0; i < nbSections; ++i) {
// Read section
fileSections[i] = std::make_unique<Section>();
fileSections[i]->nextu = nullptr;
fileSections[i]->nextPiece = nullptr;
readSection(file, *fileSections[i], fileName, nodes[fileID]);
fileSections[i]->fileSymbols = &fileSymbols;
fileSections[i]->symbols.reserve(nbSymPerSect[i]);
@@ -549,21 +525,18 @@ void obj_ReadFile(char const *fileName, unsigned int fileID) {
}
// Give patches' PC section pointers to their sections
for (uint32_t i = 0; i < nbSections; ++i) {
if (sectTypeHasData(fileSections[i]->type)) {
for (Patch &patch : fileSections[i]->patches) {
for (std::unique_ptr<Section> const &sect : fileSections) {
if (sectTypeHasData(sect->type)) {
for (Patch &patch : sect->patches) {
linkPatchToPCSect(patch, fileSections);
}
}
}
// Give symbols' section pointers to their sections
for (uint32_t i = 0; i < nbSymbols; ++i) {
if (std::holds_alternative<Label>(fileSymbols[i].data)) {
Label &label = std::get<Label>(fileSymbols[i].data);
label.section = fileSections[label.sectionID].get();
// Give the section a pointer to the symbol as well
linkSymToSect(fileSymbols[i], *label.section);
for (Symbol &sym : fileSymbols) {
if (std::holds_alternative<Label>(sym.data)) {
sym.linkToSection(*fileSections[std::get<Label>(sym.data).sectionID]);
}
}
@@ -572,23 +545,11 @@ void obj_ReadFile(char const *fileName, unsigned int fileID) {
sect_AddSection(std::move(fileSections[i]));
}
// Fix symbols' section pointers to component sections
// Fix symbols' section pointers to section "pieces"
// This has to run **after** all the `sect_AddSection()` calls,
// so that `sect_GetSection()` will work
for (uint32_t i = 0; i < nbSymbols; ++i) {
if (std::holds_alternative<Label>(fileSymbols[i].data)) {
Label &label = std::get<Label>(fileSymbols[i].data);
Section *section = label.section;
if (section->modifier != SECTION_NORMAL) {
// Associate the symbol with the main section, not the "component" one
label.section = sect_GetSection(section->name);
}
if (section->modifier == SECTION_FRAGMENT) {
// Add the fragment's offset to the symbol's
// (`section->offset` is computed by `sect_AddSection`)
label.offset += section->offset;
}
}
for (Symbol &sym : fileSymbols) {
sym.fixSectionOffset();
}
}

View File

@@ -317,12 +317,12 @@ static bool compareSymbols(SortedSymbol const &sym1, SortedSymbol const &sym2) {
template<typename F>
static void forEachSortedSection(SortedSections const &bankSections, F callback) {
for (Section const *sect : bankSections.zeroLenSections) {
for (; sect; sect = sect->nextu.get()) {
for (; sect != nullptr; sect = sect->nextPiece.get()) {
callback(*sect);
}
}
for (Section const *sect : bankSections.sections) {
for (; sect; sect = sect->nextu.get()) {
for (; sect != nullptr; sect = sect->nextPiece.get()) {
callback(*sect);
}
}
@@ -349,13 +349,14 @@ static void writeSymBank(SortedSections const &bankSections, SectionType type, u
if (sym->name.empty() || !startsIdentifier(sym->name[0])) {
continue;
}
uint16_t addr = static_cast<uint16_t>(sym->label().offset + sect.org);
assume(std::holds_alternative<Label>(sym->data));
uint16_t addr = static_cast<uint16_t>(std::get<Label>(sym->data).offset + sect.org);
uint16_t parentAddr = addr;
if (auto pos = sym->name.find('.'); pos != std::string::npos) {
std::string parentName = sym->name.substr(0, pos);
if (Symbol const *parentSym = sym_GetSymbol(parentName);
parentSym && std::holds_alternative<Label>(parentSym->data)) {
Label const &parentLabel = parentSym->label();
Label const &parentLabel = std::get<Label>(parentSym->data);
Section const &parentSection = *parentLabel.section;
parentAddr = static_cast<uint16_t>(parentLabel.offset + parentSection.org);
}
@@ -433,25 +434,30 @@ uint16_t forEachSection(SortedSections const &sectList, F callback) {
}
static void writeMapSymbols(Section const *sect) {
for (uint16_t org = sect->org; sect; sect = sect->nextu.get()) {
uint16_t org = sect->org;
for (bool announced = true; sect != nullptr; sect = sect->nextPiece.get(), announced = false) {
for (Symbol *sym : sect->symbols) {
// Don't output symbols that begin with an illegal character
if (sym->name.empty() || !startsIdentifier(sym->name[0])) {
continue;
}
// Announce this "piece" before its contents
if (!announced) {
if (sect->modifier == SECTION_UNION) {
fputs("\t ; Next union\n", mapFile);
} else if (sect->modifier == SECTION_FRAGMENT) {
fputs("\t ; Next fragment\n", mapFile);
}
announced = true;
}
assume(std::holds_alternative<Label>(sym->data));
uint32_t address = std::get<Label>(sym->data).offset + org;
// Space matches "\tSECTION: $xxxx ..."
fprintf(mapFile, "\t $%04" PRIx32 " = ", sym->label().offset + org);
fprintf(mapFile, "\t $%04" PRIx32 " = ", address);
writeSymName(sym->name, mapFile);
putc('\n', mapFile);
}
// Announce the following "piece"
if (SectionModifier mod = sect->nextu ? sect->nextu->modifier : SECTION_NORMAL;
mod == SECTION_UNION) {
fputs("\t ; Next union\n", mapFile);
} else if (mod == SECTION_FRAGMENT) {
fputs("\t ; Next fragment\n", mapFile);
}
}
}

View File

@@ -505,8 +505,10 @@ void patch_CheckAssertions() {
}
static void checkPatchSize(Patch const &patch, int32_t v, uint8_t n) {
static constexpr unsigned m = CHAR_BIT * sizeof(int);
if (n < m && (v < -(1 << n) || v >= 1 << n)) {
assume(n != 0); // That doesn't make sense
assume(n < CHAR_BIT * sizeof(int)); // Otherwise `1 << n` is UB
if (v < -(1 << n) || v >= 1 << n) {
diagnosticAt(
patch,
WARNING_TRUNCATION_1,
@@ -515,8 +517,7 @@ static void checkPatchSize(Patch const &patch, int32_t v, uint8_t n) {
v < 0 ? " (may be negative?)" : "",
n
);
return;
} else if (n < m + 1 && v < -(1 << (n - 1))) {
} else if (v < -(1 << (n - 1))) {
diagnosticAt(
patch,
WARNING_TRUNCATION_2,
@@ -568,7 +569,9 @@ static void applyFilePatches(Section &section, Section &dataSection) {
dataSection.data[offset] = jumpOffset & 0xFF;
} else {
// Patch a certain number of bytes
if (typeSize < sizeof(int)) {
checkPatchSize(patch, value, typeSize * 8);
}
for (uint8_t i = 0; i < typeSize; ++i) {
dataSection.data[offset + i] = value & 0xFF;
value >>= 8;
@@ -577,14 +580,14 @@ static void applyFilePatches(Section &section, Section &dataSection) {
}
}
// Applies all of a section's patches, iterating over "components" of unionized sections
// Applies all of a section's patches, iterating over "pieces" of unionized sections
static void applyPatches(Section &section) {
if (!sectTypeHasData(section.type)) {
return;
}
for (Section *component = &section; component; component = component->nextu.get()) {
applyFilePatches(*component, section);
for (Section *piece = &section; piece != nullptr; piece = piece->nextPiece.get()) {
applyFilePatches(*piece, section);
}
}

View File

@@ -2,9 +2,9 @@
#include "link/sdas_obj.hpp"
#include <ctype.h>
#include <inttypes.h>
#include <memory>
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
@@ -15,18 +15,13 @@
#include "helpers.hpp" // assume, literal_strlen
#include "linkdefs.hpp"
#include "platform.hpp"
#include "util.hpp" // parseWholeNumber
#include "link/fstack.hpp"
#include "link/section.hpp"
#include "link/symbol.hpp"
#include "link/warning.hpp"
enum NumberType {
HEX = 16, // X
DEC = 10, // D
OCT = 8, // Q
};
struct Location {
FileStackNode const *src;
uint32_t lineNo;
@@ -84,36 +79,26 @@ static int nextLine(std::vector<char> &lineBuf, Location &where, FILE *file) {
}
}
static uint32_t readNumber(char const *str, char const *&endptr, NumberType base) {
for (uint32_t res = 0;;) {
static char const *digits = "0123456789ABCDEF";
char const *ptr = strchr(digits, toupper(*str));
static uint64_t readNumber(Location const &where, char const *str, NumberBase base) {
std::optional<uint64_t> res = parseWholeNumber(str, base);
if (!ptr || ptr - digits >= base) {
endptr = str;
return res;
}
++str;
res = res * base + (ptr - digits);
}
}
static uint32_t parseNumber(Location const &where, char const *str, NumberType base) {
if (str[0] == '\0') {
fatalAt(where, "Expected number, got empty string");
}
char const *endptr;
uint32_t res = readNumber(str, endptr, base);
if (*endptr != '\0') {
if (!res) {
fatalAt(where, "Expected number, got \"%s\"", str);
}
return res;
return *res;
}
static uint8_t parseByte(Location const &where, char const *str, NumberType base) {
uint32_t num = parseNumber(where, str, base);
static uint32_t readInt(Location const &where, char const *str, NumberBase base) {
uint64_t num = readNumber(where, str, base);
if (num > UINT32_MAX) {
fatalAt(where, "\"%s\" is not an int", str);
}
return num;
}
static uint8_t readByte(Location const &where, char const *str, NumberBase base) {
uint64_t num = readNumber(where, str, base);
if (num > UINT8_MAX) {
fatalAt(where, "\"%s\" is not a byte", str);
@@ -184,18 +169,18 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
int lineType = nextLine(line, where, file);
// The first letter (thus, the line type) identifies the integer type
NumberType numberType;
NumberBase numberBase;
switch (lineType) {
case EOF:
fatalAt(where, "SDCC object only contains comments and empty lines");
case 'X':
numberType = HEX;
numberBase = BASE_16;
break;
case 'D':
numberType = DEC;
numberBase = BASE_10;
break;
case 'Q':
numberType = OCT;
numberBase = BASE_8;
break;
default:
fatalAt(
@@ -239,12 +224,12 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
// Expected format: "A areas S global symbols"
getToken(line.data(), "Empty 'H' line");
uint32_t expectedNbAreas = parseNumber(where, token, numberType);
uint32_t expectedNbAreas = readInt(where, token, numberBase);
expectToken("areas", 'H');
getToken(nullptr, "'H' line is too short");
uint32_t expectedNbSymbols = parseNumber(where, token, numberType);
uint32_t expectedNbSymbols = readInt(where, token, numberBase);
fileSymbols.reserve(expectedNbSymbols);
expectToken("global", 'H');
@@ -296,7 +281,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
getToken(nullptr, "'A' line is too short");
uint32_t tmp = parseNumber(where, token, numberType);
uint32_t tmp = readInt(where, token, numberBase);
if (tmp > UINT16_MAX) {
fatalAt(
@@ -310,7 +295,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
expectToken("flags", 'A');
getToken(nullptr, "'A' line is too short");
tmp = parseNumber(where, token, numberType);
tmp = readInt(where, token, numberBase);
if (tmp & (1 << AREA_PAGING)) {
fatalAt(where, "Paging is not supported");
}
@@ -329,7 +314,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
expectToken("addr", 'A');
getToken(nullptr, "'A' line is too short");
tmp = parseNumber(where, token, numberType);
tmp = readInt(where, token, numberBase);
curSection->org = tmp; // Truncation keeps the address portion only
curSection->bank = tmp >> 16;
@@ -364,7 +349,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
}
curSection->isAlignFixed = false; // No such concept!
curSection->fileSymbols = &fileSymbols; // IDs are instead per-section
curSection->nextu = nullptr;
curSection->nextPiece = nullptr;
fileSections.push_back({.section = std::move(curSection), .writeIndex = 0});
break;
@@ -386,7 +371,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
getToken(nullptr, "'S' line is too short");
if (int32_t value = parseNumber(where, &token[3], numberType); !fileSections.empty()) {
if (int32_t value = readInt(where, &token[3], numberBase); !fileSections.empty()) {
// Symbols in sections are labels; their value is an offset
Section *section = fileSections.back().section.get();
if (section->isAddressFixed) {
@@ -465,7 +450,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
data.clear();
for (token = strtok(line.data(), delim); token; token = strtok(nullptr, delim)) {
data.push_back(parseByte(where, token, numberType));
data.push_back(readByte(where, token, numberBase));
}
if (data.size() < addrSize) {
@@ -487,9 +472,9 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
uint16_t areaIdx;
getToken(nullptr, "'R' line is too short");
areaIdx = parseByte(where, token, numberType);
areaIdx = readByte(where, token, numberBase);
getToken(nullptr, "'R' line is too short");
areaIdx |= static_cast<uint16_t>(parseByte(where, token, numberType)) << 8;
areaIdx |= static_cast<uint16_t>(readByte(where, token, numberBase)) << 8;
if (areaIdx >= fileSections.size()) {
fatalAt(
where,
@@ -549,16 +534,16 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
// appropriate RPN expression (depending on flags), plus an addition for the
// bytes being patched over.
while ((token = strtok(nullptr, delim)) != nullptr) {
uint16_t flags = parseByte(where, token, numberType);
uint16_t flags = readByte(where, token, numberBase);
if ((flags & 0xF0) == 0xF0) {
getToken(nullptr, "Incomplete relocation");
flags = (flags & 0x0F)
| static_cast<uint16_t>(parseByte(where, token, numberType)) << 4;
| static_cast<uint16_t>(readByte(where, token, numberBase)) << 4;
}
getToken(nullptr, "Incomplete relocation");
uint8_t offset = parseByte(where, token, numberType);
uint8_t offset = readByte(where, token, numberBase);
if (offset < addrSize) {
fatalAt(
@@ -578,10 +563,10 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
}
getToken(nullptr, "Incomplete relocation");
uint16_t idx = parseByte(where, token, numberType);
uint16_t idx = readByte(where, token, numberBase);
getToken(nullptr, "Incomplete relocation");
idx |= static_cast<uint16_t>(parseByte(where, token, numberType));
idx |= static_cast<uint16_t>(readByte(where, token, numberBase));
// Loudly fail on unknown flags
if (flags & (1 << RELOC_ZPAGE | 1 << RELOC_NPAGE)) {
@@ -902,21 +887,10 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
sect_AddSection(std::move(section));
}
// Fix symbols' section pointers to component sections
// Fix symbols' section pointers to section "pieces"
// This has to run **after** all the `sect_AddSection()` calls,
// so that `sect_GetSection()` will work
for (Symbol &sym : fileSymbols) {
if (std::holds_alternative<Label>(sym.data)) {
Label &label = std::get<Label>(sym.data);
if (Section *section = label.section; section->modifier != SECTION_NORMAL) {
if (section->modifier == SECTION_FRAGMENT) {
// Add the fragment's offset to the symbol's
// (`section->offset` is computed by `sect_AddSection`)
label.offset += section->offset;
}
// Associate the symbol with the main section, not the "component" one
label.section = sect_GetSection(section->name);
}
}
sym.fixSectionOffset();
}
}

View File

@@ -6,9 +6,7 @@
#include <memory>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -56,7 +54,7 @@ static void checkAgainstFixedAddress(Section const &target, Section const &other
}
}
static bool checkAgainstFixedAlign(Section const &target, Section const &other, int32_t ofs) {
static bool checkAgainstFixedAlign(Section const &target, Section const &other, uint32_t ofs) {
if (target.isAddressFixed) {
if ((target.org - ofs) & other.alignMask) {
fatalTwoAt(
@@ -109,10 +107,7 @@ static void checkFragmentCompat(Section &target, Section &other) {
target.isAddressFixed = true;
target.org = org;
} else if (other.isAlignFixed) {
int32_t ofs = (other.alignOfs - target.size) % (other.alignMask + 1);
if (ofs < 0) {
ofs += other.alignMask + 1;
}
uint32_t ofs = (other.alignOfs - target.size) & other.alignMask;
if (checkAgainstFixedAlign(target, other, ofs)) {
target.isAlignFixed = true;
target.alignMask = other.alignMask;
@@ -191,10 +186,10 @@ static void mergeSections(Section &target, std::unique_ptr<Section> &&other) {
}
// LCOV_EXCL_STOP
// Note that the order in which fragments are stored in the `nextu` list does not
// Note that the order in which fragments are stored in the `nextPiece` list does not
// really matter, only that offsets were properly computed above
other->nextu = std::move(target.nextu);
target.nextu = std::move(other);
other->nextPiece = std::move(target.nextPiece);
target.nextPiece = std::move(other);
}
void sect_AddSection(std::unique_ptr<Section> &&section) {

View File

@@ -90,3 +90,43 @@ void sym_TraceLocalAliasedSymbols(std::string const &name) {
}
}
}
void Symbol::linkToSection(Section &section) {
assume(std::holds_alternative<Label>(data));
Label &label = std::get<Label>(data);
// Link the symbol to the section
label.section = &section;
// Link the section to the symbol, keeping the section's symbol list sorted
uint32_t a = 0, b = section.symbols.size();
while (a != b) {
uint32_t c = (a + b) / 2;
assume(std::holds_alternative<Label>(section.symbols[c]->data));
Label const &other = std::get<Label>(section.symbols[c]->data);
if (other.offset > label.offset) {
b = c;
} else {
a = c + 1;
}
}
section.symbols.insert(section.symbols.begin() + a, this);
}
void Symbol::fixSectionOffset() {
if (!std::holds_alternative<Label>(data)) {
return;
}
Label &label = std::get<Label>(data);
Section *section = label.section;
assume(section);
if (section->modifier != SECTION_NORMAL) {
// Associate the symbol with the main section, not the "piece"
label.section = sect_GetSection(section->name);
}
if (section->modifier == SECTION_FRAGMENT) {
// Add the fragment's offset to the symbol's
// (`section->offset` is computed by `sect_AddSection`)
label.offset += section->offset;
}
}

View File

@@ -186,11 +186,11 @@ void warning(FileStackNode const *src, uint32_t lineNo, WarningID id, char const
break;
case WarningBehavior::ENABLED:
printDiag(src, lineNo, fmt, args, "warning", STYLE_RED, "[-W%s]", flag);
printDiag(src, lineNo, fmt, args, "warning", STYLE_YELLOW, "[-W%s]", flag);
break;
case WarningBehavior::ERROR:
printDiag(src, lineNo, fmt, args, "error", STYLE_YELLOW, "[-Werror=%s]", flag);
printDiag(src, lineNo, fmt, args, "error", STYLE_RED, "[-Werror=%s]", flag);
warnings.incrementErrors();
break;

View File

@@ -7,14 +7,47 @@
#include <stdlib.h>
#include "helpers.hpp"
#include "platform.hpp"
#include "style.hpp"
static constexpr size_t maxLineLen = 79;
// LCOV_EXCL_START
#if defined(_MSC_VER) || defined(__MINGW32__)
#define WIN32_LEAN_AND_MEAN // Include less from `windows.h`
#include <windows.h>
#else
#include <sys/ioctl.h>
#endif
void Usage::printAndExit(int code) const {
FILE *file = code ? stderr : stdout;
FILE *file;
bool isTerminal;
if (code) {
file = stderr;
isTerminal = isatty(STDERR_FILENO);
} else {
file = stdout;
isTerminal = isatty(STDOUT_FILENO);
}
// Use the console window width minus 1 as the maximum line length for flags,
// or the historically common 80 minus 1 if the output is not to a console TTY
size_t maxLineLen = 79;
if (isTerminal) {
// LCOV_EXCL_START
#if defined(_MSC_VER) || defined(__MINGW32__)
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
if (csbi.srWindow.Right > csbi.srWindow.Left) {
maxLineLen = static_cast<size_t>(csbi.srWindow.Right - csbi.srWindow.Left);
}
#else
struct winsize winSize;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &winSize);
if (winSize.ws_col > 1) {
maxLineLen = static_cast<size_t>(winSize.ws_col - 1);
}
#endif
// LCOV_EXCL_STOP
}
// Print "Usage: <program name>"
style_Set(file, STYLE_GREEN, true);
@@ -34,6 +67,7 @@ void Usage::printAndExit(int code) const {
fprintf(file, " %s", flag.c_str());
flagsWidth += 1 + flag.length();
}
style_Reset(file);
fputs("\n\n", file);
// Measure the options' flags
@@ -50,12 +84,13 @@ void Usage::printAndExit(int code) const {
padOpts = pad;
}
}
int optIndent = static_cast<int>(literal_strlen(" ") + padOpts);
size_t optOffset = literal_strlen(" ") + padOpts;
// Print the options
if (!options.empty()) {
style_Set(file, STYLE_GREEN, true);
fputs("Useful options:\n", file);
style_Reset(file);
}
for (auto const &[opts, description] : options) {
fputs(" ", file);
@@ -64,23 +99,36 @@ void Usage::printAndExit(int code) const {
size_t optWidth = 0;
for (size_t i = 0; i < opts.size(); ++i) {
if (i > 0) {
style_Reset(file);
fputs(", ", file);
optWidth += literal_strlen(", ");
}
style_Set(file, STYLE_CYAN, false);
fputs(opts[i].c_str(), file);
style_Reset(file);
optWidth += opts[i].length();
}
if (optWidth < padOpts) {
// Measure the description lines
size_t descLen = 0;
for (std::string const &descLine : description) {
if (descLine.length() > descLen) {
descLen = descLine.length();
}
}
// If the description lines would wrap around the console, put them on their own lines
size_t optIndent = optOffset;
if (optIndent + literal_strlen(" ") + descLen > maxLineLen) {
optIndent = 6;
fprintf(file, "\n%*c", static_cast<int>(optIndent), ' ');
} else if (optWidth < padOpts) {
fprintf(file, "%*c", static_cast<int>(padOpts - optWidth), ' ');
}
// Print the description lines, indented to the same level
for (size_t i = 0; i < description.size(); ++i) {
style_Reset(file);
if (i > 0) {
fprintf(file, "\n%*c", optIndent, ' ');
fprintf(file, "\n%*c", static_cast<int>(optIndent), ' ');
}
fprintf(file, " %s", description[i].c_str());
}
@@ -88,7 +136,6 @@ void Usage::printAndExit(int code) const {
}
// Print the link for further help information
style_Reset(file);
fputs("\nFor more help, use \"", file);
style_Set(file, STYLE_CYAN, true);
fprintf(file, "man %s", name.c_str());
@@ -114,5 +161,3 @@ void Usage::printAndExit(char const *fmt, ...) const {
printAndExit(1);
}
// LCOV_EXCL_STOP

View File

@@ -2,8 +2,13 @@
#include "util.hpp"
#include <errno.h>
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <string.h> // strspn
#include "helpers.hpp" // assume
bool isNewline(int c) {
return c == '\r' || c == '\n';
@@ -21,14 +26,26 @@ bool isPrintable(int c) {
return c >= ' ' && c <= '~';
}
bool isUpper(int c) {
return c >= 'A' && c <= 'Z';
}
bool isLower(int c) {
return c >= 'a' && c <= 'z';
}
bool isLetter(int c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
return isUpper(c) || isLower(c);
}
bool isDigit(int c) {
return c >= '0' && c <= '9';
}
bool isBinDigit(int c) {
return c == '0' || c == '1';
}
bool isOctDigit(int c) {
return c >= '0' && c <= '7';
}
@@ -51,6 +68,103 @@ bool continuesIdentifier(int c) {
return startsIdentifier(c) || isDigit(c) || c == '#' || c == '$' || c == '@';
}
uint8_t parseHexDigit(int c) {
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
} else if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
} else {
assume(isDigit(c));
return c - '0';
}
}
// Parses a number from a string, moving the pointer to skip the parsed characters.
std::optional<uint64_t> parseNumber(char const *&str, NumberBase base) {
// Identify the base if not specified
// Does *not* support '+' or '-' sign prefix (unlike `strtoul` and `std::from_chars`)
if (base == BASE_AUTO) {
base = BASE_10;
// Skips leading blank space (like `strtoul`)
str += strspn(str, " \t");
// Supports traditional ("0b", "0o", "0x") and RGBASM ('%', '&', '$') base prefixes
switch (str[0]) {
case '%':
base = BASE_2;
++str;
break;
case '&':
base = BASE_8;
++str;
break;
case '$':
base = BASE_16;
++str;
break;
case '0':
switch (str[1]) {
case 'B':
case 'b':
base = BASE_2;
str += 2;
break;
case 'O':
case 'o':
base = BASE_8;
str += 2;
break;
case 'X':
case 'x':
base = BASE_16;
str += 2;
break;
}
break;
}
}
// Get the digit-condition function corresponding to the base
bool (*canParseDigit)(int c) = base == BASE_2 ? isBinDigit
: base == BASE_8 ? isOctDigit
: base == BASE_10 ? isDigit
: base == BASE_16 ? isHexDigit
: nullptr; // LCOV_EXCL_LINE
assume(canParseDigit != nullptr);
char const * const startDigits = str;
// Parse the number one digit at a time
// Does *not* support '_' digit separators
uint64_t result = 0;
for (; canParseDigit(str[0]); ++str) {
uint8_t digit = parseHexDigit(str[0]);
if (result > (UINT64_MAX - digit) / base) {
// Skip remaining digits and set errno = ERANGE on overflow
while (canParseDigit(str[0])) {
++str;
}
result = UINT64_MAX;
errno = ERANGE;
break;
}
result = result * base + digit;
}
// Return the parsed number if there were any digit characters
if (str - startDigits == 0) {
return std::nullopt;
}
return result;
}
// Parses a number from an entire string, returning nothing if there are more unparsed characters.
std::optional<uint64_t> parseWholeNumber(char const *str, NumberBase base) {
std::optional<uint64_t> result = parseNumber(str, base);
return str[0] == '\0' ? result : std::nullopt;
}
char const *printChar(int c) {
// "'A'" + '\0': 4 bytes
// "'\\n'" + '\0': 5 bytes

Binary file not shown.

View File

@@ -0,0 +1,15 @@
macro careful
if _NARG == 20
warn "You're in too deep!"
else
careful \#, deeper
endc
endm
careful surface
macro recurse
recurse
endm
rept 3
recurse
endr

View File

@@ -0,0 +1,4 @@
warning: You're in too deep! [-Wuser]
at backtrace-collapsed.asm::careful(3) <- backtrace-collapsed.asm::careful(5) <- backtrace-collapsed.asm::careful(5) <- ...16 more... <- backtrace-collapsed.asm::careful(5) <- backtrace-collapsed.asm(8)
FATAL: Recursion limit (64) exceeded
at backtrace-collapsed.asm::recurse(11) <- backtrace-collapsed.asm::recurse(11) <- backtrace-collapsed.asm::recurse(11) <- ...60 more... <- backtrace-collapsed.asm::REPT~1(14) <- backtrace-collapsed.asm(13)

View File

@@ -0,0 +1 @@
-B 5 -B collapse

View File

@@ -1,13 +1,13 @@
def s equs "d"
charmap "A", 1
charmap "B", 2
charmap "c{s}e", 3
charmap "F", 4, 5, 6
charmap 'A', 1
charmap 'B', 2
charmap 'c{s}e', 3
charmap 'F', 4, 5, 6
charmap "'", 42
charmap "\"", 1234
charmap "\n\r\t\0", 1337
charmap "',\",\\", 99
charmap '"', 1234
charmap '\n\r\t\0', 1337
charmap '\',\",\\', 99
MACRO char
assert (\1) == (\2)

View File

@@ -1,4 +1,4 @@
opt Wno-unmapped-char, Wno-obsolete
opt Wno-unmapped-char
charmap "<NULL>", $00
charmap "A", $10
charmap "B", $20

View File

@@ -0,0 +1,24 @@
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(16)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(16)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(17)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(17)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(17)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(18)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(26)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(26)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(27)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(27)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(27)
warning: Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead [-Wobsolete]
at charlen-strchar.asm(28)

View File

@@ -0,0 +1 @@
FATAL: Dependency files can only be created if a target file is specified with either '-o', '-MQ' or '-MT'

View File

@@ -0,0 +1 @@
-M depfile inputfile

View File

@@ -0,0 +1 @@
FATAL: Empty feature for option '-s'

View File

@@ -0,0 +1 @@
-s :-

View File

@@ -0,0 +1 @@
FATAL: Invalid argument for option '-B'

View File

@@ -0,0 +1 @@
-B nan

View File

@@ -0,0 +1 @@
FATAL: Invalid argument for option '-Q'

View File

@@ -0,0 +1 @@
-Q invalid

View File

@@ -0,0 +1 @@
FATAL: Invalid argument for option '-X'

View File

@@ -0,0 +1 @@
-X 0c777

View File

@@ -0,0 +1 @@
FATAL: Must specify exactly 2 characters for option '-b'

View File

@@ -0,0 +1 @@
-b 01X

View File

@@ -0,0 +1 @@
FATAL: Invalid argument for option '--color'

View File

@@ -0,0 +1 @@
--color=always --color=never --color=auto --color=yes

View File

@@ -0,0 +1 @@
FATAL: Must specify exactly 4 characters for option '-g'

View File

@@ -0,0 +1 @@
-g 0123X

View File

@@ -0,0 +1 @@
FATAL: Invalid argument for option '-p'

View File

@@ -0,0 +1 @@
-p 123abc

View File

@@ -0,0 +1 @@
FATAL: Invalid argument for option '-r'

View File

@@ -0,0 +1 @@
-r nan

View File

@@ -0,0 +1 @@
FATAL: Invalid feature for option '-s': "invalid"

View File

@@ -0,0 +1 @@
-s invalid:-

View File

@@ -0,0 +1 @@
FATAL: Invalid argument for option '-s'

View File

@@ -0,0 +1 @@
-s invalid

View File

@@ -0,0 +1,17 @@
FATAL: More than one input file specified
Usage: rgbasm [-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>
Useful 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
For more help, use "man rgbasm" or go to https://rgbds.gbdev.io/docs/

View File

@@ -0,0 +1 @@
one two

View File

@@ -0,0 +1 @@
FATAL: Argument for option '-Q' must be between 1 and 31

View File

@@ -0,0 +1 @@
-Q .999

View File

@@ -0,0 +1 @@
FATAL: Argument for option '-p' must be between 0 and 0xFF

View File

@@ -0,0 +1 @@
-p 999

View File

@@ -0,0 +1 @@
FATAL: Argument for option '-r' is out of range

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