Compare commits

..

59 Commits

Author SHA1 Message Date
Rangi42
8b85875b67 Release v0.9.3 2025-06-30 15:08:04 -04:00
Rangi
7054d81650 Implement grayscale DMG palette specs (#1709) 2025-06-30 14:53:05 -04:00
Rangi
5942117ac3 Avoid generating phony dependencies for files that don't exist (#1708) 2025-06-29 16:42:24 -04:00
Rangi42
e7a3b9d90e Format rgbgfx -vvvvvv string visually 2025-06-21 14:16:48 -04:00
Rangi
b13d623ad4 Encode reversed PNG images as grayscale or indexed when possible (#1703) 2025-06-19 09:48:27 -04:00
Rangi
37bf9fae01 Only define parse.lac for Bison 3.5 or greater (#1702) 2025-06-14 17:01:16 -04:00
Rangi42
612cf3b7dd Fix some formatting 2025-06-12 17:27:08 -04:00
Rangi
089e366ddc Implement CHARVAL function (#1701) 2025-06-12 17:21:12 -04:00
Rangi
fa9e29e4ce Implement ++ operator for string concatenation (#1698) 2025-06-12 22:52:00 +02:00
Antonio Vivace
fa3d83a3d1 ci container build: when pushing a version-tagged build, overwrite 'latest' as well 2025-06-08 18:21:58 +02:00
Rangi
804db4e073 Handle missing newline at EOF for linkerscript INCLUDEd files (#1691) 2025-05-22 10:55:58 +02:00
Rangi
5d998ef483 Restrict custom binary and graphics digits (#1693)
* Restrict custom binary and graphics digits

* Update documentation

* Fix build error
2025-05-22 10:52:51 +02:00
Rangi
126b1e5726 Reuse startsIdentifier and continuesIdentifier functions (#1695) 2025-05-19 15:31:26 -04:00
Rangi
4f2400c15b Hint to {interpolate} names when EQUS expanding does not occur (#1692) 2025-05-18 17:53:34 +02:00
Antonio Vivace
063d284cbf Dockerfile: explain commands 2025-05-16 18:48:08 +02:00
Antonio Vivace
205bf5a11d Dockerfile: install the compiled tools after the compilation (#1690) 2025-05-16 18:48:08 +02:00
Rangi
41c94aa448 Omit the version number from distrbuted release archive filenames (#1685) 2025-05-06 13:28:54 +02:00
Rangi
d413870e6d .sym file sorting accounts for local labels' parents' addresses and names (#1684) 2025-05-05 13:57:25 -04:00
Rangi
e95ac6fb06 Recover from errors even inside REPT/FOR loops (#1683) 2025-05-04 17:51:53 -04:00
Rangi
e1ae92709c Fix STRSLICE with no stop index argument (#1682) 2025-05-04 16:56:25 -04:00
Rangi42
1715f85d50 Release v0.9.2 2025-05-04 10:04:11 -04:00
Rangi42
c2de0a991a Update test dependency 2025-05-03 12:43:57 -04:00
Rangi
2e6e1ccf06 Show specific messages for some more invalid instructions, not just "syntax error" (#1679) 2025-05-03 12:31:00 -04:00
Rangi42
081f48404c Remove a TODO comment about overlapping proto-palettes
The original algorithm makes a simplifying assumption that one
proto-palette does not fully contain another, and we have no
particular reason to violate this condition.
2025-05-03 10:19:33 -04:00
Rangi42
bdac0ce053 Remove unplanned TODO comments in src/gfx/pal_spec.cpp
ACT palette files support a transparent color index, but RGBGFX
cannot apply *one* such transparent color; it would need every
palette's first color to be transparent. Also ACT files tend to
say the first color is transparent anyway, which the user may
not have intended.

ACO palette files can specify version 2 color data, but it's
required to come after version 1 data, and the colors themselves
already exist in the earlier v1 data; v2 just adds UTF-16 names.

Thus, we do not need to be handling these data in those formats.
2025-05-02 21:29:14 -04:00
Rangi
122d91509f Clear some more TODO comments (#1677) 2025-05-02 21:06:34 -04:00
Rangi
7c6f778ae7 Take care of miscellaneous commented TODOs (#1676) 2025-05-02 16:44:12 -04:00
Eldred Habert
8cf6c5423a Implement --background-color (#1508)
Co-authored-by: Rangi42 <sylvie.oukaour+rangi42@gmail.com>
2025-05-01 23:39:52 -04:00
Rangi
56f7222230 Don't output anonymous labels in map files (#1674) 2025-05-01 19:19:25 +02:00
Rangi
e45b9625ca Group sequences of garbage characters (#1672) 2025-04-30 23:31:41 -04:00
Rangi42
e0a8eb8aff Update test dependencies 2025-04-24 09:52:08 -04:00
Rangi
2a5b9b5f98 Fix two RGBGFX bugs (#1671)
* Fix two RGBGFX bugs

* Fix clang-format idempotence

* Update src/gfx/rgba.cpp

Co-authored-by: Eldred Habert <me@eldred.fr>

---------

Co-authored-by: Eldred Habert <me@eldred.fr>
2025-04-24 15:39:14 +02:00
Rangi42
a72843748f Avoid using indirect C++ types 2025-04-23 00:53:20 -04:00
Rangi42
762e2311d2 Add test case for FOR loop variable reusing an existing one 2025-04-22 15:10:50 -04:00
Rangi
0b7cda9e0c Allow negative values to count macro arguments from the end (#1670) 2025-04-20 00:37:50 -04:00
Rangi42
df83bc31d2 Consistently use PRId* not PRIi* 2025-04-19 23:44:34 -04:00
John Millikin
bc8d99d915 Add -o / --output option to rgbfix to write separate output files (#1666) 2025-04-19 23:17:11 -04:00
Rangi42
c841672059 Don't use tabs for alignment 2025-03-31 19:13:46 -04:00
Rangi42
75b605797d Fix rgblink(5) man page syntax error
Copied from https://salsa.debian.org/twolife/rgbds/-/blob/640a5293/debian/patches/groff.patch
2025-03-07 10:37:20 -05:00
Rangi42
00d0ae840d Avoid use of goto in nextLine 2025-02-27 14:28:17 -05:00
Rangi42
2cdbb145da Avoid use of goto in shiftChar 2025-02-27 14:17:01 -05:00
Rangi42
d8192560b0 Avoid use of goto in FormatSpec::useCharacter 2025-02-27 13:45:13 -05:00
Rangi42
9b395f3bf1 Fix double negative 2025-02-23 13:36:55 -05:00
Rangi
0150eb4bf3 Exclude more lines from test coverage (#1663)
These fall into a few categories:
- `_unreachable()`
- Verbose print messages
- Errors that should never practically occur (alloc/read/write failure,
  more than UINT32_MAX anonymous labels, etc)
2025-02-17 04:56:10 -05:00
Rangi
632342b254 Use LCOV_EXCL comments to exclude some lines from test coverage (#1662) 2025-02-16 13:56:55 -05:00
Rangi
c9060c7f2d Increase test coverage (#1661) 2025-02-16 09:29:16 -05:00
Rangi
993879a2ed Derive operator!= from operator== (#1660) 2025-02-15 12:37:42 +01:00
Rangi
62309d5c87 Define operator!= in terms of operator== (#1659) 2025-02-15 11:34:06 +01:00
Rangi
b2e865ee2a Disable EQUS expansion for raw symbols (by parsing them as strings) (#1648) 2025-02-15 10:44:51 +01:00
Rangi
3feb75f84f Implement new string functions (#1655)
`STRFIND`, `STRRFIND`, `STRCHAR`, `STRSLICE`, `CHARCMP`, `CHARSIZE`, and `REVCHAR`
2025-02-14 23:09:45 +01:00
Rangi42
ad4d9da4cf Remove unnecessary default constructor definitions 2025-02-14 18:58:34 +01:00
Rangi42
1489854932 Use more const references when possible 2025-02-14 18:58:34 +01:00
Rangi
2aef09c8d9 Allow the bit/res/set bit index to be determined at link time (#1654)
This increments the object file revision number from 11 to 12
since it adds a new `RPN_BIT_INDEX` command.
2025-02-12 17:14:10 +01:00
Rangi42
48412e9c56 Some miscellaneous refactoring and copy-editing 2025-02-10 16:51:51 +01:00
Rangi
177a3abfac Fix bug where macro names can be treated as numeric symbols (#1653) 2025-02-08 23:03:21 +01:00
Rangi
4c916b8da8 Parser refers to "symbol"s, "label"s, and "local label"s, not "identifier"s (#1652)
This better matches how the lexed tokens are discussed in rgbasm(5)
2025-02-06 18:01:33 +01:00
Rangi42
d9d381cb62 Refactor the parser to have fewer *_no_str intermediate rules 2025-02-04 18:59:11 +01:00
Rangi
fbde24ee17 Add contrib/checkformat.bash to check for clang-formatting (#1646) 2025-02-04 10:40:38 +01:00
Rangi
91310c9eb6 Update the post-release checklist to mention rgbds-live (#1647) 2025-02-04 09:59:03 +01:00
233 changed files with 2724 additions and 1074 deletions

View File

@@ -38,6 +38,7 @@ jobs:
sed -i "2i LABEL org.opencontainers.image.description=\"RGBDS container image for the release version $TAG_NAME\"" Dockerfile
docker build . --tag ghcr.io/gbdev/rgbds:$TAG_NAME
docker push ghcr.io/gbdev/rgbds:$TAG_NAME
docker push ghcr.io/gbdev/rgbds:latest
- name: Delete untagged container images
if: github.repository_owner == 'gbdev'

12
.github/workflows/checkformat.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Code format checking
on: pull_request
jobs:
checkformat:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Check format
run: |
./contrib/checkformat.bash

View File

@@ -61,12 +61,12 @@ jobs:
cmake --install build --verbose --prefix install_dir --strip
- name: Package binaries
run: |
Compress-Archive -LiteralPath @("install_dir/bin/rgbasm.exe", "install_dir/bin/rgblink.exe", "install_dir/bin/rgbfix.exe", "install_dir/bin/rgbgfx.exe", "install_dir/bin/zlib1.dll", "install_dir/bin/libpng16.dll") "rgbds-${{ env.version }}-win${{ matrix.bits }}.zip"
Compress-Archive -LiteralPath @("install_dir/bin/rgbasm.exe", "install_dir/bin/rgblink.exe", "install_dir/bin/rgbfix.exe", "install_dir/bin/rgbgfx.exe", "install_dir/bin/zlib1.dll", "install_dir/bin/libpng16.dll") "rgbds-win${{ matrix.bits }}.zip"
- name: Upload Windows binaries
uses: actions/upload-artifact@v4
with:
name: win${{ matrix.bits }}
path: rgbds-${{ env.version }}-win${{ matrix.bits }}.zip
path: rgbds-win${{ matrix.bits }}.zip
macos:
runs-on: macos-14
@@ -92,15 +92,15 @@ jobs:
strip rgb{asm,link,fix,gfx}
- name: Package binaries
run: |
zip --junk-paths rgbds-${{ env.version }}-macos.zip rgb{asm,link,fix,gfx} man/* .github/scripts/install.sh
zip --junk-paths rgbds-macos.zip rgb{asm,link,fix,gfx} man/* .github/scripts/install.sh
- name: Upload macOS binaries
uses: actions/upload-artifact@v4
with:
name: macos
path: rgbds-${{ env.version }}-macos.zip
path: rgbds-macos.zip
linux:
runs-on: ubuntu-20.04 # Oldest supported, for best glibc compatibility.
runs-on: ubuntu-22.04 # Oldest supported, for best glibc compatibility.
steps:
- name: Get version from tag
shell: bash
@@ -112,19 +112,19 @@ jobs:
- name: Install deps
shell: bash
run: |
./.github/scripts/install_deps.sh ubuntu-20.04
./.github/scripts/install_deps.sh ubuntu-22.04
- name: Build binaries
run: |
make -kj WARNFLAGS="-Wall -Wextra -pedantic -static" PKG_CONFIG="pkg-config --static" Q=
strip rgb{asm,link,fix,gfx}
- name: Package binaries
run: |
tar caf rgbds-${{ env.version }}-linux-x86_64.tar.xz --transform='s#.*/##' rgb{asm,link,fix,gfx} man/* .github/scripts/install.sh
tar caf rgbds-linux-x86_64.tar.xz --transform='s#.*/##' rgb{asm,link,fix,gfx} man/* .github/scripts/install.sh
- name: Upload Linux binaries
uses: actions/upload-artifact@v4
with:
name: linux
path: rgbds-${{ env.version }}-linux-x86_64.tar.xz
path: rgbds-linux-x86_64.tar.xz
release:
runs-on: ubuntu-latest
@@ -155,11 +155,11 @@ jobs:
draft: true # Don't publish the release quite yet...
prerelease: ${{ contains(github.ref, '-rc') }}
files: |
win32/rgbds-${{ env.version }}-win32.zip
win64/rgbds-${{ env.version }}-win64.zip
macos/rgbds-${{ env.version }}-macos.zip
linux/rgbds-${{ env.version }}-linux-x86_64.tar.xz
rgbds-${{ env.version }}.tar.gz
win32/rgbds-win32.zip
win64/rgbds-win64.zip
macos/rgbds-macos.zip
linux/rgbds-linux-x86_64.tar.xz
rgbds-source.tar.gz
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -7,7 +7,7 @@ jobs:
unix:
strategy:
matrix:
os: [ubuntu-20.04, ubuntu-22.04, macos-14]
os: [ubuntu-22.04, macos-14]
cxx: [g++, clang++]
buildsys: [make, cmake]
exclude:

1
.gitignore vendored
View File

@@ -14,4 +14,5 @@ CMakeCache.txt
CMakeFiles/
cmake_install.cmake
build/
*.dSYM/
callgrind.out.*

View File

@@ -51,16 +51,15 @@ else()
add_link_options(${SAN_FLAGS})
add_definitions(-D_GLIBCXX_ASSERTIONS)
# A non-zero optimization level is desired in debug mode, but allow overriding it nonetheless
# TODO: this overrides anything previously set... that's a bit sloppy!
set(CMAKE_CXX_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls ${CMAKE_CXX_FLAGS_DEBUG}"
CACHE STRING "" FORCE)
endif()
if(MORE_WARNINGS)
add_compile_options(-Werror -Wextra
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wold-style-cast -Wshift-overflow=2
-Wstringop-overflow=4 -Wundef -Wuninitialized -Wunused
-Wshadow # TODO: -Wshadow=compatible-local?
-Wstringop-overflow=4 -Wtrampolines -Wundef -Wuninitialized -Wunused -Wshadow
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1
-Wno-format-nonliteral -Wno-strict-overflow
-Wno-unused-but-set-variable # bison's `yynerrs_` is incremented but unused

View File

@@ -1,6 +1,6 @@
FROM debian:12-slim
LABEL org.opencontainers.image.source=https://github.com/gbdev/rgbds
ARG version=0.9.1
ARG version=0.9.3
WORKDIR /rgbds
COPY . .
@@ -8,7 +8,14 @@ COPY . .
RUN apt-get update && \
apt-get install sudo make cmake gcc build-essential -y
# Install dependencies and compile RGBDS
RUN ./.github/scripts/install_deps.sh ubuntu-22.04
RUN make -j CXXFLAGS="-O3 -flto -DNDEBUG -static" PKG_CONFIG="pkg-config --static" Q=
RUN tar caf rgbds-${version}-linux-x86_64.tar.xz --transform='s#.*/##' rgbasm rgblink rgbfix rgbgfx man/* .github/scripts/install.sh
# Create an archive with the compiled executables and all the necessary to install it,
# so it can be copied outside of the container and installed/used in another system
RUN tar caf rgbds-linux-x86_64.tar.xz --transform='s#.*/##' rgbasm rgblink rgbfix rgbgfx man/* .github/scripts/install.sh
# Install RGBDS on the container so all the executables will be available in the PATH
RUN cp man/* .
RUN ./.github/scripts/install.sh

View File

@@ -202,7 +202,7 @@ develop:
$Q${MAKE} WARNFLAGS="${WARNFLAGS} -Werror -Wextra \
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond \
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wold-style-cast -Wshift-overflow=2 \
-Wstringop-overflow=4 -Wundef -Wuninitialized -Wunused -Wshadow \
-Wstringop-overflow=4 -Wtrampolines -Wundef -Wuninitialized -Wunused -Wshadow \
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \
-Wno-format-nonliteral -Wno-strict-overflow -Wno-unused-but-set-variable \
-Wno-type-limits -Wno-tautological-constant-out-of-range-compare -Wvla \
@@ -265,4 +265,4 @@ wine-shim:
dist:
$Qgit ls-files | sed s~^~$${PWD##*/}/~ \
| tar -czf rgbds-`git -c safe.directory='*' describe --tags | cut -c 2-`.tar.gz -C .. -T -
| tar -czf rgbds-source.tar.gz -C .. -T -

View File

@@ -72,9 +72,14 @@ GitHub.
8. Update the following related projects.
- [rgbobj](https://github.com/gbdev/rgbobj) and [rgbds-obj](https://github.com/gbdev/rgbds-obj):
make sure that object files created by the latest RGBASM can be parsed and displayed.
If the object file revision has been updated, rgbobj will need a corresponding release.
- [rgbds-www](https://github.com/gbdev/rgbds-www): update
1. [rgbds-www](https://github.com/gbdev/rgbds-www): update
[src/pages/versions.mdx](https://github.com/gbdev/rgbds-www/blob/master/src/pages/versions.mdx)
to list the new release.
2. [rgbds-live](https://github.com/gbdev/rgbds-live): update the `rgbds` submodule (and
[patches/rgbds.patch](https://github.com/gbdev/rgbds-live/blob/master/patches/rgbds.patch)
if necessary) to use the new release, and
[index.html](https://github.com/gbdev/rgbds-live/blob/master/index.html)
to link to the new manual version.
3. [rgbobj](https://github.com/gbdev/rgbobj) and [rgbds-obj](https://github.com/gbdev/rgbds-obj):
make sure that object files created by the latest RGBASM can be parsed and displayed.
If the object file revision has been updated, `rgbobj` will need a corresponding release.

View File

@@ -21,6 +21,7 @@ _rgbfix_completions() {
[l]="old-licensee:unk"
[m]="mbc-type:mbc"
[n]="rom-version:unk"
[o]="output:glob-*.gb *.gbc *.sgb"
[p]="pad-value:unk"
[r]="ram-size:unk"
[t]="title:unk"

View File

@@ -19,6 +19,7 @@ _rgbgfx_completions() {
[Z]="columns:normal"
[a]="attr-map:glob-*.attrmap"
[A]="auto-attr-map:normal"
[B]="background-color:unk"
[b]="base-tiles:unk"
[c]="colors:unk"
[d]="depth:unk"

15
contrib/checkformat.bash Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
clang-format --version
find . -type f \( -iname '*.hpp' -o -iname '*.cpp' \) -exec clang-format -i {} +
if ! git diff-index --quiet HEAD --; then
echo 'Unformatted files:'
git diff-index --name-only HEAD --
echo
git diff HEAD --
return 1
fi

View File

@@ -32,14 +32,15 @@ diff <(xxd "$1") <(xxd "$2") | while read -r LINE; do
# Ignore comment lines, only pick matching bank
# (The bank regex ignores comments already, make `cut` and `tr` process less lines)
grep -Ei "$(printf "^%02x:" $BANK)" "$SYMFILE" |
sed "s/$(printf "^%02x:" $BANK)/0x/g" |
cut -d ';' -f 1 |
tr -d "\r" |
sort -g |
while read -r SYMADDR SYM; do
SYMADDR=$((0x${SYMADDR#*:}))
SYMADDR=$(($SYMADDR))
if [[ $SYMADDR -le $ADDR ]]; then
printf " (%s+0x%x)\n" "$SYM" $((ADDR - SYMADDR))
fi
# TODO: assumes sorted sym files
done | tail -n 1
fi)
printf "%02x:%04x %s\n" $BANK $ADDR "$EXTRA"

View File

@@ -53,6 +53,7 @@ local args=(
'(-L --logo)'{-L,--logo}'+[Set custom logo]:1bpp image:'
'(-m --mbc-type)'{-m,--mbc-type}"+[Set MBC flags]:mbc name:_mbc_names"
'(-n --rom-version)'{-n,--rom-version}'+[Set ROM version]:rom version byte:'
'(-o --output)'{-o,--output}"+[Output file]:output file:_files -g '*.{gb,sgb,gbc}'"
'(-p --pad-value)'{-p,--pad-value}'+[Pad to next valid size using this byte as padding]:padding byte:'
'(-r --ram-size)'{-r,--ram-size}'+[Set RAM size]:ram size byte:'
'(-t --title)'{-t,--title}'+[Set title string]:11-char title string:'

View File

@@ -28,6 +28,7 @@ local args=(
'(-Z --columns)'{-Z,--columns}'[Read the image in column-major order]'
'(-a --attr-map -A --auto-attr-map)'{-a,--attr-map}'+[Generate a map of tile attributes (mirroring)]:attrmap file:_files'
'(-B --background-color)'{-B,--background-color}'+[Ignore tiles containing only specified color]:color:'
'(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:'
'(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:'
'(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths'

View File

@@ -3,6 +3,7 @@
#ifndef RGBDS_ASM_CHARMAP_HPP
#define RGBDS_ASM_CHARMAP_HPP
#include <optional>
#include <stdint.h>
#include <string>
#include <string_view>
@@ -20,8 +21,11 @@ void charmap_Push();
void charmap_Pop();
void charmap_CheckStack();
void charmap_Add(std::string const &mapping, std::vector<int32_t> &&value);
bool charmap_HasChar(std::string const &input);
bool charmap_HasChar(std::string const &mapping);
size_t charmap_CharSize(std::string const &mapping);
std::optional<int32_t> charmap_CharValue(std::string const &mapping, size_t idx);
std::vector<int32_t> charmap_Convert(std::string const &input);
size_t charmap_ConvertNext(std::string_view &input, std::vector<int32_t> *output);
std::string charmap_Reverse(std::vector<int32_t> const &value, bool &unique);
#endif // RGBDS_ASM_CHARMAP_HPP

View File

@@ -71,7 +71,6 @@ void fstk_RunFor(
int32_t reptLineNo,
ContentSpan const &span
);
void fstk_StopRept();
bool fstk_Break();
void fstk_NewRecursionDepth(size_t newDepth);

View File

@@ -118,17 +118,8 @@ struct LexerState {
extern char binDigits[2];
extern char gfxDigits[4];
static inline void lexer_SetBinDigits(char const digits[2]) {
binDigits[0] = digits[0];
binDigits[1] = digits[1];
}
static inline void lexer_SetGfxDigits(char const digits[4]) {
gfxDigits[0] = digits[0];
gfxDigits[1] = digits[1];
gfxDigits[2] = digits[2];
gfxDigits[3] = digits[3];
}
void lexer_SetBinDigits(char const digits[2]);
void lexer_SetGfxDigits(char const digits[4]);
bool lexer_AtTopLevel();
void lexer_RestartRept(uint32_t lineNo);

View File

@@ -9,11 +9,11 @@
#include <vector>
struct MacroArgs {
unsigned int shift;
uint32_t shift;
std::vector<std::shared_ptr<std::string>> args;
uint32_t nbArgs() const { return args.size() - shift; }
std::shared_ptr<std::string> getArg(uint32_t i) const;
std::shared_ptr<std::string> getArg(int32_t i) const;
std::shared_ptr<std::string> getAllArgs() const;
void appendArg(std::shared_ptr<std::string> arg);

View File

@@ -22,15 +22,6 @@ struct Expression {
std::vector<uint8_t> rpn{}; // Bytes serializing the RPN expression
uint32_t rpnPatchSize = 0; // Size the expression will take in the object file
Expression() = default;
Expression(Expression &&) = default;
#ifdef _MSC_VER
// MSVC and WinFlexBison won't build without this...
Expression(Expression const &) = default;
#endif
Expression &operator=(Expression &&) = default;
bool isKnown() const { return data.holds<int32_t>(); }
int32_t value() const { return data.get<int32_t>(); }
@@ -51,6 +42,7 @@ struct Expression {
bool makeCheckHRAM();
void makeCheckRST();
void makeCheckBitIndex(uint8_t mask);
void checkNBit(uint8_t n) const;

View File

@@ -91,11 +91,11 @@ void sect_ByteString(std::vector<int32_t> const &string);
void sect_WordString(std::vector<int32_t> const &string);
void sect_LongString(std::vector<int32_t> const &string);
void sect_Skip(uint32_t skip, bool ds);
void sect_RelByte(Expression &expr, uint32_t pcShift);
void sect_RelBytes(uint32_t n, std::vector<Expression> &exprs);
void sect_RelWord(Expression &expr, uint32_t pcShift);
void sect_RelLong(Expression &expr, uint32_t pcShift);
void sect_PCRelByte(Expression &expr, uint32_t pcShift);
void sect_RelByte(Expression const &expr, uint32_t pcShift);
void sect_RelBytes(uint32_t n, std::vector<Expression> const &exprs);
void sect_RelWord(Expression const &expr, uint32_t pcShift);
void sect_RelLong(Expression const &expr, uint32_t pcShift);
void sect_PCRelByte(Expression const &expr, uint32_t pcShift);
void sect_BinaryFile(std::string const &name, int32_t startPos);
void sect_BinaryFileSlice(std::string const &name, int32_t startPos, int32_t length);

View File

@@ -82,7 +82,6 @@ Symbol *sym_RedefEqu(std::string const &symName, int32_t value);
Symbol *sym_AddVar(std::string const &symName, int32_t value);
int32_t sym_GetRSValue();
void sym_SetRSValue(int32_t value);
uint32_t sym_GetConstantValue(std::string const &symName);
// Find a symbol by exact name, bypassing expansion checks
Symbol *sym_FindExactSymbol(std::string const &symName);
// Find a symbol, possibly scoped, by name

View File

@@ -103,7 +103,7 @@ public:
} else if (other._tag == other._t2.tag_value) {
*this = other._t2.value;
} else {
_tag = nulltag;
_tag = nulltag; // LCOV_EXCL_LINE
}
return *this;
}

View File

@@ -18,12 +18,10 @@
#include "gfx/main.hpp"
class File {
// Construct a `std::streambuf *` by default, since it's probably lighter than a `filebuf`.
Either<std::streambuf *, std::filebuf> _file;
public:
File() {}
~File() { close(); }
File() : _file(nullptr) {}
// This should only be called once, and before doing any `->` operations.
// Returns `nullptr` on error, and a non-null pointer otherwise.
@@ -61,20 +59,6 @@ public:
return const_cast<File *>(this)->operator->();
}
File *close() {
if (_file.holds<std::filebuf>()) {
// This is called by the destructor, and an explicit `close` shouldn't close twice.
std::filebuf fileBuf = std::move(_file.get<std::filebuf>());
_file.emplace<std::streambuf *>(nullptr);
if (fileBuf.close() != nullptr) {
return this;
}
} else if (_file.get<std::streambuf *>() != nullptr) {
return this;
}
return nullptr;
}
char const *c_str(std::string const &path) const {
return _file.holds<std::filebuf>() ? path.c_str()
: _file.get<std::streambuf *>() == std::cin.rdbuf() ? "<stdin>"

View File

@@ -10,6 +10,8 @@
#include <utility>
#include <vector>
#include "helpers.hpp"
#include "gfx/rgba.hpp"
struct Options {
@@ -21,13 +23,16 @@ struct Options {
uint8_t verbosity = 0; // -v
std::string attrmap{}; // -a, -A
std::optional<Rgba> bgColor{}; // -B
std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
enum {
NO_SPEC,
EXPLICIT,
EMBEDDED,
DMG,
} palSpecType = NO_SPEC; // -c
std::vector<std::array<std::optional<Rgba>, 4>> palSpec{};
uint8_t palSpecDmg = 0;
uint8_t bitDepth = 2; // -d
std::string inputTileset{}; // -i
struct {
@@ -62,6 +67,12 @@ struct Options {
mutable bool hasTransparentPixels = false;
uint8_t maxOpaqueColors() const { return nbColorsPerPal - hasTransparentPixels; }
uint8_t dmgColors[4] = {};
uint8_t dmgValue(uint8_t i) const {
assume(i < 4);
return (palSpecDmg >> (2 * i)) & 0b11;
}
};
extern Options options;

View File

@@ -3,7 +3,10 @@
#ifndef RGBDS_GFX_PAL_SPEC_HPP
#define RGBDS_GFX_PAL_SPEC_HPP
void parseInlinePalSpec(char const * const arg);
void parseInlinePalSpec(char const * const rawArg);
void parseExternalPalSpec(char const *arg);
void parseDmgPalSpec(char const * const rawArg);
void parseBackgroundPalSpec(char const *arg);
#endif // RGBDS_GFX_PAL_SPEC_HPP

View File

@@ -36,8 +36,8 @@ struct Rgba {
auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
}
bool operator==(Rgba const &rhs) const { return toCSS() == rhs.toCSS(); }
bool operator!=(Rgba const &rhs) const { return toCSS() != rhs.toCSS(); }
// CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
// Since the rest of the bits don't matter then, we return 0x8000 exactly.

View File

@@ -25,7 +25,6 @@ class EnumSeq {
auto operator*() const { return _value; }
bool operator==(Iterator const &rhs) const { return _value == rhs._value; }
bool operator!=(Iterator const &rhs) const { return _value != rhs._value; }
};
public:
@@ -59,10 +58,6 @@ public:
bool operator==(ZipIterator const &rhs) const {
return std::get<0>(_iters) == std::get<0>(rhs._iters);
}
bool operator!=(ZipIterator const &rhs) const {
return std::get<0>(_iters) != std::get<0>(rhs._iters);
}
};
template<typename... Ts>

View File

@@ -9,7 +9,7 @@
#include "helpers.hpp" // assume
#define RGBDS_OBJECT_VERSION_STRING "RGB9"
#define RGBDS_OBJECT_REV 11U
#define RGBDS_OBJECT_REV 12U
enum AssertionType { ASSERT_WARN, ASSERT_ERROR, ASSERT_FATAL };
@@ -52,6 +52,7 @@ enum RPNCommand {
RPN_HRAM = 0x60,
RPN_RST = 0x61,
RPN_BIT_INDEX = 0x62,
RPN_HIGH = 0x70,
RPN_LOW = 0x71,

View File

@@ -3,6 +3,9 @@
#ifndef RGBDS_UTIL_HPP
#define RGBDS_UTIL_HPP
bool startsIdentifier(int c);
bool continuesIdentifier(int c);
char const *printChar(int c);
#endif // RGBDS_UTIL_HPP

View File

@@ -5,7 +5,7 @@
#define PACKAGE_VERSION_MAJOR 0
#define PACKAGE_VERSION_MINOR 9
#define PACKAGE_VERSION_PATCH 1
#define PACKAGE_VERSION_PATCH 3
char const *get_package_version_string();

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt GBZ80 7
.Os
.Sh NAME

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt RGBASM-OLD 5
.Os
.Sh NAME

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt RGBASM 1
.Os
.Sh NAME
@@ -51,8 +51,19 @@ is invalid because it could also be
The arguments are as follows:
.Bl -tag -width Ds
.It Fl b Ar chars , Fl \-binary-digits Ar chars
Change the two characters used for binary constants.
The defaults are 01.
Allow two characters to be used for binary constants in addition to the default
.Sq 0
and
.Sq 1 .
Valid characters are numbers other than
.Sq 0
and
.Sq 1 ,
letters,
.Sq \&. ,
.Sq # ,
or
.Sq @ .
.It Fl D Ar name Ns Oo = Ns Ar value Oc , Fl \-define Ar name Ns Oo = Ns Ar value Oc
Add a string symbol to the compiled source code.
This is equivalent to
@@ -65,7 +76,21 @@ is not specified.
.It Fl E , Fl \-export-all
Export all labels, including unreferenced and local labels.
.It Fl g Ar chars , Fl \-gfx-chars Ar chars
Change the four characters used for gfx constants.
Allow four characters to be used for graphics constants in addition to the default
.Sq 0 ,
.Sq 1 ,
.Sq 2 ,
and
.Sq 3 .
Valid characters are numbers other than
.Sq 0
to
.Sq 3 ,
letters,
.Sq \&. ,
.Sq # ,
or
.Sq @ .
The defaults are 0123.
.It Fl h , Fl \-help
Print help text for the program and exit.
@@ -275,7 +300,7 @@ This warning is enabled by
.Fl Wall .
.It Fl Wbuiltin-args
Warn about incorrect arguments to built-in functions, such as
.Fn STRSUB
.Fn STRSLICE
with indexes outside of the string's bounds.
This warning is enabled by
.Fl Wall .

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt RGBASM 5
.Os
.Sh NAME
@@ -548,7 +548,7 @@ There are a number of escape sequences you can use within a string:
.El
.Pp
Multi-line strings are contained in triple quotes
.Pq Ql \&"\&"\&"for instance\&"\&"\&" .
.Pq Ql \&"\&"\&"for instance""" .
Escape sequences work the same way in multi-line strings; however, literal newline characters will be included as-is, without needing to escape them with
.Ql \er
or
@@ -560,26 +560,30 @@ Inside them, backslashes and braces are treated like regular characters, so they
For example, the raw string
.Ql #"\et\e1{s}\e"
is equivalent to the regular string
.Ql "\e\et\e\e1\e{s}\e\e" .
.Ql \&"\e\et\e\e1\e{s}\e\e" .
(Note that this prevents raw strings from including the double quote character.)
Raw strings also may be contained in triple quotes for them to be multi-line, so they can include literal newline or quote characters (although still not three quotes in a row).
.Pp
The following functions operate on string expressions.
Most of them return a string, however some of these functions actually return an integer and can be used as part of an integer expression!
.Bl -column "STRSUB(str, pos, len)"
You can use the
.Sq ++
operator to concatenate two strings.
.Ql \&"str" ++ \&"ing"
is equivalent to
.Ql \&"string" ,
or to
.Ql STRCAT("str", \&"ing") .
.Pp
The following functions operate on string expressions, and return strings themselves.
.Bl -column "STRSLICE(str, start, stop)"
.It Sy Name Ta Sy Operation
.It Fn STRLEN str Ta Returns the number of characters in Ar str .
.It Fn STRCAT strs... Ta Concatenates Ar strs .
.It Fn STRCMP str1 str2 Ta Returns -1 if Ar str1 No is alphabetically lower than Ar str2 No , zero if they match, 1 if Ar str1 No is greater than Ar str2 .
.It Fn STRIN str1 str2 Ta Returns the first position of Ar str2 No in Ar str1 No or zero if it's not present Pq first character is position 1 .
.It Fn STRRIN str1 str2 Ta Returns the last position of Ar str2 No in Ar str1 No or zero if it's not present Pq first character is position 1 .
.It Fn STRSUB str pos len Ta Returns a substring from Ar str No starting at Ar pos No (first character is position 1, last is position -1) and Ar len No characters long. If Ar len No is not specified the substring continues to the end of Ar str .
.It Fn STRUPR str Ta Returns Ar str No with all ASCII letters
.Pq Ql a-z
in uppercase.
.It Fn STRLWR str Ta Returns Ar str No with all ASCII letters
.Pq Ql A-Z
in lowercase.
.It Fn STRSLICE str start stop Ta Returns a substring of Ar str No starting at Ar start No and ending at Ar stop No (exclusive). If Ar stop No is not specified, the substring continues to the end of Ar str .
.It Fn STRRPL str old new Ta Returns Ar str No with each non-overlapping occurrence of the substring Ar old No replaced with Ar new .
.It Fn STRFMT fmt args... Ta Returns the string Ar fmt No with each
.Ql %spec
@@ -589,9 +593,42 @@ pattern replaced by interpolating the format
with its corresponding argument in
.Ar args
.Pq So %% Sc is replaced by the So % Sc character .
.It Fn INCHARMAP str Ta Returns 1 if Ar str No has an entry in the current charmap, and 0 otherwise .
.It Fn STRCHAR str idx Ta Returns the substring of Ar str No for the charmap entry at Ar idx No with the current charmap . Pq Ar idx No counts charmap entries, not characters.
.It Fn REVCHAR vals... Ta Returns the string that is mapped to Ar vals No with the current charmap. If there is no unique charmap entry for Ar vals Ns , an error occurs.
.El
.Pp
The following functions operate on string expressions, but return integers.
.Bl -column "STRRFIND(str, sub)"
.It Sy Name Ta Sy Operation
.It Fn STRLEN str Ta Returns the number of characters in Ar str .
.It Fn STRCMP str1 str2 Ta Compares Ar str1 No and Ar str2 No according to ASCII ordering of their characters. Returns -1 if Ar str1 No is lower than Ar str2 Ns , 1 if Ar str1 No is greater than Ar str2 Ns , or 0 if they match.
.It Fn STRFIND str sub Ta Returns the first index of Ar sub No in Ar str Ns , or -1 if it's not present.
.It Fn STRRFIND str sub Ta Returns the last index of Ar sub No in Ar str Ns , or -1 if it's not present.
.It Fn INCHARMAP str Ta Returns 1 if Ar str No has an entry in the current charmap, or 0 otherwise.
.It Fn CHARLEN str Ta Returns the number of charmap entries in Ar str No with the current charmap.
.It Fn CHARSUB str pos Ta Returns the substring for the charmap entry at Ar pos No in Ar str No (first character is position 1, last is position -1) with the current charmap .
.It Fn CHARCMP str1 str2 Ta Compares Ar str1 No and Ar str2 No according to their charmap entry values with the current charmap. Returns -1 if Ar str1 No is lower than Ar str2 Ns , 1 if Ar str1 No is greater than Ar str2 Ns , or 0 if they match.
.It Fn CHARSIZE char Ta Returns how many values are in the charmap entry for Ar char No with the current charmap.
.It Fn CHARVAL char idx Ta Returns the value at Ar idx No of the charmap entry for Ar char .
.El
.Pp
Note that indexes count starting from 0 at the beginning, or from -1 at the end.
The characters of a string are counted by
.Ql STRLEN ;
the charmap entries of a string are counted by
.Ql CHARLEN ;
and the values of a charmap entry are counted by
.Ql CHARSIZE .
.Pp
The following legacy functions are similar to other functions that operate on string expressions, but for historical reasons, they count starting from
.Em position 1 ,
not from index 0!
(Position -1 still counts from the end.)
.Bl -column "STRSUB(str, pos, len)"
.It Sy Name Ta Sy Operation
.It Fn STRSUB str pos len Ta Returns a substring of Ar str No starting at Ar pos No and Ar len No characters long. If Ar len No is not specified, the substring continues to the end of Ar str No .
.It Fn STRIN str sub Ta Returns the first position of Ar sub No in Ar str Ns , or 0 if it's not present.
.It Fn STRRIN str sub Ta Returns the last position of Ar sub No in Ar str Ns , or 0 if it's not present.
.It Fn CHARSUB str pos Ta Returns the substring of Ar str No for the charmap entry at Ar pos No with the current charmap . Pq Ar pos No counts charmap entries, not characters.
.El
.Ss Character maps
When writing text strings that are meant to be displayed on the Game Boy, the character encoding in the ROM may need to be different than the source file encoding.
@@ -1052,7 +1089,7 @@ and
.Ic WRAMX
types are still considered different.
.It
Different constraints (alignment, bank, etc.) can be specified for each unionized section declaration, but they must all be compatible.
Different constraints (alignment, bank, etc.) can be specified for each section fragment declaration, but they must all be compatible.
For example, alignment must be compatible with any fixed address, all specified banks must be the same, etc.
.It
A section fragment may not be unionized; after all, that wouldn't make much sense.
@@ -1104,7 +1141,9 @@ Additionally, label names can contain up to a single dot
.Ql \&. ,
which may not be the first character.
.Pp
A symbol cannot have the same name as a reserved keyword, unless it is prefixed by a hash
A symbol cannot have the same name as a reserved keyword, unless its name is a
.Dq raw identifier
prefixed by a hash
.Sq # .
For example,
.Ql #load
@@ -1279,7 +1318,7 @@ it at the same time.
below).
.Ss Numeric constants
.Ic EQU
is used to define immutable numeric symbols.
is used to define numeric constant symbols.
Unlike
.Sq =
above, constants defined this way cannot be redefined.
@@ -1387,6 +1426,8 @@ This expansion is disabled in a few contexts:
and
.Ql MACRO name
will not expand string constants in their names.
Expansion is also disabled if the string constant's name is a raw identifier prefixed by a hash
.Sq # .
.Bd -literal -offset indent
DEF COUNTREG EQUS "[hl+]"
ld a, COUNTREG
@@ -1852,9 +1893,11 @@ being the second, and so on. Since there are only nine digits, you can only use
To use the rest, you put the argument number in angle brackets, like
.Ic \e<10> .
.Pp
This bracketed syntax supports decimal numbers and numeric constant symbols.
This bracketed syntax supports decimal numbers and numeric symbols, where negative values count from the last argument.
For example,
.Ql \e<_NARG>
or
.Ql \e<-1>
will get the last argument.
.Pp
Other macro arguments and symbol interpolations will also be expanded inside the angle brackets.

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt RGBDS 5
.Os
.Sh NAME
@@ -388,10 +388,19 @@ The value is then ANDed with $00FF
check.
Checks if the value is a valid
.Ql rst
.Pq see Do RST vec Dc in Xr gbz80 7
vector, that is one of $00, $08, $10, $18, $20, $28, $30, or $38.
vector
.Pq see Do RST vec Dc in Xr gbz80 7 ,
that is, one of $00, $08, $10, $18, $20, $28, $30, or $38.
The value is then ORed with $C7
.Pq Ql \&| $C7 .
.It Li $62 Ta Ql bit/res/set
check; followed by the instruction's
.Cm BYTE
mask.
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.

View File

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

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt RGBFIX 1
.Os
.Sh NAME
@@ -17,6 +17,7 @@
.Op Fl l Ar licensee_id
.Op Fl m Ar mbc_type
.Op Fl n Ar rom_version
.Op Fl o Ar out_file
.Op Fl p Ar pad_value
.Op Fl r Ar ram_size
.Op Fl t Ar title_str
@@ -134,6 +135,9 @@ Set the ROM version
to a given value from 0 to 0xFF.
.It Fl O , Fl \-overwrite
Allow overwriting different non-zero bytes in the header without a warning being emitted.
.It Fl o Ar out_file , Fl \-output Ar out_file
Write the modified ROM image to the given file, or '-' to write to standard output.
If not specified, the input files are modified in-place, or written to standard output if read from standard input.
.It Fl p Ar pad_value , Fl \-pad-value Ar pad_value
Pad the ROM image to a valid size with a given pad value from 0 to 255 (0xFF).
.Nm

View File

@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt RGBGFX 1
.Os
.Sh NAME
@@ -103,6 +103,19 @@ and has the same size.
Same as
.Fl a Ar base_path Ns .attrmap
.Pq see Sx Automatic output paths .
.It Fl B Ar color , Fl \-background-color Ar color
Set a background color to be omitted from output.
Colors are accepted in
.Ql #rgb
or
.Ql #rrggbb
format, or as
.Ql transparent .
Input tiles which are entirely the specified background color are ignored and will not be output in tile data file.
The tilemap, atrribute map, or palette map files
.Em will
use placeholder values where background tiles were.
If a background color is specified, it cannot be used within tiles which are not ignored.
.It Fl b Ar base_ids , Fl \-base-tiles Ar base_ids
Set the base IDs for tile map output.
.Ar base_ids
@@ -126,7 +139,7 @@ begins with a hash character
.Ql # ,
it is treated as an inline palette specification.
It should contain a comma-separated list of hexadecimal colors, each beginning with a hash.
Colors are accepted either as
Colors are accepted in
.Ql #rgb
or
.Ql #rrggbb
@@ -146,6 +159,24 @@ is the case-insensitive word
then the first four colors of the input PNG's embedded palette are used.
It is an error if the PNG is not indexed, or if colors other than these 4 are used.
.Pq This is different from the default behavior of indexed PNGs, as then unused entries in the embedded palette are ignored, whereas they are not with Fl c Cm embedded .
.It Sy DMG palette spec
If
.Ar pal_spec
starts with case-insensitive
.Cm dmg= ,
then the following two-digit hexadecimal number specifies four grayscale DMG color indexes.
The number functions like the DMG's $FF47
.Sy BGP
register
(see
.Lk https://gbdev.io/pandocs/Palettes.html Pan Docs
for more information):
the low two bits 0-1 specify which gray shade goes in color index 0,
the next two bits 2-3 specify which gray shade goes in color index 1,
and so on.
Gray shade 0 is the lightest (white), 3 is the darkest (black).
The same gray shade cannot go in two color indexes.
To specify a DMG palette, the input PNG must have all its colors in shades of gray, without any transparent colors.
.It Sy external palette spec
Otherwise,
.Ar pal_spec
@@ -515,6 +546,8 @@ Otherwise, if the PNG only contains shades of gray, they will be categorized int
.Dq bins
as there are colors per palette, and the palette is set to these bins.
The darkest gray will end up in bin #0, and so on; note that this is the opposite of the RGB method below.
This is equivalent to having specified a DMG palette of
.Fl c Cm dmg=E4 .
If two distinct grays end up in the same bin, the RGB method is used instead.
.Pp
Be careful that

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt RGBLINK 1
.Os
.Sh NAME
@@ -121,8 +121,8 @@ WRAMX sections that are fixed to a bank other than 1 become errors, other WRAMX
Disables padding the end of the final file.
This option automatically enables
.Fl t .
You can use this when not not making a ROM.
When making a ROM, be careful that not using this is not a replacement for
You can use this to make binary files that are not a ROM.
When making a ROM, note that not using this is not a replacement for
.Xr rgbfix 1 Ap s Fl p
option!
.El

View File

@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd February 2, 2025
.Dd June 30, 2025
.Dt RGBLINK 5
.Os
.Sh NAME
@@ -120,7 +120,7 @@ causes all sections between it and the next
.Ic ORG
or bank specification to be placed at addresses automatically determined by
.Nm .
.Pq It is, however, compatible with Ic ALIGN No below.
.Pq \&It is, however, compatible with Ic ALIGN No below.
.Pp
.Ql Ic ALIGN Ar addr , Ar offset
increases the

View File

@@ -9,10 +9,10 @@ set(common_src
)
find_package(BISON 3.0.0 REQUIRED)
set(BISON_FLAGS "-Wall -Dparse.lac=full -Dlr.type=ielr")
set(BISON_FLAGS "-Wall -Dlr.type=ielr")
# Set some optimization flags on versions that support them
if(BISON_VERSION VERSION_GREATER_EQUAL "3.5")
set(BISON_FLAGS "${BISON_FLAGS} -Dapi.token.raw=true")
set(BISON_FLAGS "${BISON_FLAGS} -Dparse.lac=full -Dapi.token.raw=true")
endif()
if(BISON_VERSION VERSION_GREATER_EQUAL "3.6")
set(BISON_FLAGS "${BISON_FLAGS} -Dparse.error=detailed")

View File

@@ -31,6 +31,29 @@ struct CharmapNode {
struct Charmap {
std::string name;
std::vector<CharmapNode> nodes; // first node is reserved for the root node
// Traverse the trie depth-first to derive the character mappings in definition order
template<typename F>
bool forEachChar(F callback) const {
// clang-format off: nested initializers
for (std::stack<std::pair<size_t, std::string>> prefixes({{0, ""}}); !prefixes.empty();) {
// clang-format on
auto [nodeIdx, mapping] = std::move(prefixes.top());
prefixes.pop();
CharmapNode const &node = nodes[nodeIdx];
if (node.isTerminal()) {
if (!callback(nodeIdx, mapping)) {
return false;
}
}
for (unsigned c = 0; c < std::size(node.next); c++) {
if (size_t nextIdx = node.next[c]; nextIdx) {
prefixes.push({nextIdx, mapping + static_cast<char>(c)});
}
}
}
return true;
}
};
static std::deque<Charmap> charmapList;
@@ -44,24 +67,12 @@ bool charmap_ForEach(
void (*charFunc)(std::string const &, std::vector<int32_t>)
) {
for (Charmap const &charmap : charmapList) {
// Traverse the trie depth-first to derive the character mappings in definition order
std::map<size_t, std::string> mappings;
// clang-format off: nested initializers
for (std::stack<std::pair<size_t, std::string>> prefixes({{0, ""}});
!prefixes.empty();) {
// clang-format on
auto [nodeIdx, mapping] = std::move(prefixes.top());
prefixes.pop();
CharmapNode const &node = charmap.nodes[nodeIdx];
if (node.isTerminal()) {
charmap.forEachChar([&mappings](size_t nodeIdx, std::string const &mapping) {
mappings[nodeIdx] = mapping;
}
for (unsigned c = 0; c < 256; c++) {
if (size_t nextIdx = node.next[c]; nextIdx) {
prefixes.push({nextIdx, mapping + static_cast<char>(c)});
}
}
}
return true;
});
mapFunc(charmap.name);
for (auto [nodeIdx, mapping] : mappings) {
charFunc(mapping, charmap.nodes[nodeIdx].value);
@@ -163,11 +174,11 @@ void charmap_Add(std::string const &mapping, std::vector<int32_t> &&value) {
std::swap(node.value, value);
}
bool charmap_HasChar(std::string const &input) {
bool charmap_HasChar(std::string const &mapping) {
Charmap const &charmap = *currentCharmap;
size_t nodeIdx = 0;
for (char c : input) {
for (char c : mapping) {
nodeIdx = charmap.nodes[nodeIdx].next[static_cast<uint8_t>(c)];
if (!nodeIdx) {
@@ -178,6 +189,34 @@ bool charmap_HasChar(std::string const &input) {
return charmap.nodes[nodeIdx].isTerminal();
}
static CharmapNode const *charmapEntry(std::string const &mapping) {
Charmap const &charmap = *currentCharmap;
size_t nodeIdx = 0;
for (char c : mapping) {
nodeIdx = charmap.nodes[nodeIdx].next[static_cast<uint8_t>(c)];
if (!nodeIdx) {
return nullptr;
}
}
return &charmap.nodes[nodeIdx];
}
size_t charmap_CharSize(std::string const &mapping) {
CharmapNode const *node = charmapEntry(mapping);
return node && node->isTerminal() ? node->value.size() : 0;
}
std::optional<int32_t> charmap_CharValue(std::string const &mapping, size_t idx) {
if (CharmapNode const *node = charmapEntry(mapping);
node && node->isTerminal() && idx < node->value.size()) {
return node->value[idx];
}
return std::nullopt;
}
std::vector<int32_t> charmap_Convert(std::string const &input) {
std::vector<int32_t> output;
for (std::string_view inputView = input; charmap_ConvertNext(inputView, &output);) {}
@@ -263,3 +302,20 @@ size_t charmap_ConvertNext(std::string_view &input, std::vector<int32_t> *output
input = input.substr(inputIdx);
return matchLen;
}
std::string charmap_Reverse(std::vector<int32_t> const &value, bool &unique) {
Charmap const &charmap = *currentCharmap;
std::string revMapping;
unique = charmap.forEachChar([&](size_t nodeIdx, std::string const &mapping) {
if (charmap.nodes[nodeIdx].value == value) {
if (revMapping.empty()) {
revMapping = mapping;
} else {
revMapping.clear();
return false;
}
}
return true;
});
return revMapping;
}

View File

@@ -22,29 +22,29 @@ void FormatSpec::useCharacter(int c) {
case ' ':
case '+':
if (state > FORMAT_SIGN) {
goto invalid;
break;
}
state = FORMAT_EXACT;
sign = c;
break;
return;
// exact
case '#':
if (state > FORMAT_EXACT) {
goto invalid;
break;
}
state = FORMAT_ALIGN;
exact = true;
break;
return;
// align
case '-':
if (state > FORMAT_ALIGN) {
goto invalid;
break;
}
state = FORMAT_WIDTH;
alignLeft = true;
break;
return;
// pad, width, and prec values
case '0':
@@ -71,27 +71,27 @@ void FormatSpec::useCharacter(int c) {
} else if (state == FORMAT_PREC) {
precision = precision * 10 + (c - '0');
} else {
goto invalid;
}
break;
}
return;
// width
case '.':
if (state > FORMAT_WIDTH) {
goto invalid;
break;
}
state = FORMAT_FRAC;
hasFrac = true;
break;
return;
// prec
case 'q':
if (state > FORMAT_PREC) {
goto invalid;
break;
}
state = FORMAT_PREC;
hasPrec = true;
break;
return;
// type
case 'd':
@@ -103,19 +103,20 @@ void FormatSpec::useCharacter(int c) {
case 'f':
case 's':
if (state >= FORMAT_DONE) {
goto invalid;
break;
}
state = FORMAT_DONE;
valid = true;
type = c;
break;
return;
default:
invalid:
break;
}
state = FORMAT_INVALID;
valid = false;
}
}
void FormatSpec::finishCharacters() {
if (!isValid()) {

View File

@@ -119,18 +119,11 @@ void fstk_SetPreIncludeFile(std::string const &path) {
warnx("Overriding pre-included filename %s", preIncludeName.c_str());
}
preIncludeName = path;
// LCOV_EXCL_START
if (verbose) {
printf("Pre-included filename %s\n", preIncludeName.c_str());
}
}
static void printDep(std::string const &path) {
if (dependFile) {
fprintf(dependFile, "%s: %s\n", targetFileName.c_str(), path.c_str());
if (generatePhonyDeps) {
fprintf(dependFile, "%s:\n", path.c_str());
}
}
// LCOV_EXCL_STOP
}
static bool isValidFilePath(std::string const &path) {
@@ -138,6 +131,15 @@ static bool isValidFilePath(std::string const &path) {
return stat(path.c_str(), &statBuf) == 0 && !S_ISDIR(statBuf.st_mode); // Reject directories
}
static void printDep(std::string const &path) {
if (dependFile) {
fprintf(dependFile, "%s: %s\n", targetFileName.c_str(), path.c_str());
if (generatePhonyDeps && isValidFilePath(path)) {
fprintf(dependFile, "%s:\n", path.c_str());
}
}
}
std::optional<std::string> fstk_FindFile(std::string const &path) {
for (std::string &incPath : includePaths) {
if (std::string fullPath = incPath + path; isValidFilePath(fullPath)) {
@@ -308,9 +310,11 @@ void fstk_RunInclude(std::string const &path, bool preInclude) {
if (!fullPath) {
if (generatedMissingIncludes && !preInclude) {
// LCOV_EXCL_START
if (verbose) {
printf("Aborting (-MG) on INCLUDE file '%s' (%s)\n", path.c_str(), strerror(errno));
}
// LCOV_EXCL_STOP
failedOnMissingInclude = true;
} else {
error("Unable to open included file '%s': %s\n", path.c_str(), strerror(errno));
@@ -319,7 +323,7 @@ void fstk_RunInclude(std::string const &path, bool preInclude) {
}
if (!newFileContext(*fullPath, false)) {
fatalerror("Failed to set up lexer for file include\n");
fatalerror("Failed to set up lexer for file include\n"); // LCOV_EXCL_LINE
}
}
@@ -388,17 +392,13 @@ void fstk_RunFor(
context.forName = symName;
}
void fstk_StopRept() {
contextStack.top().nbReptIters = 0; // Prevent more iterations
}
bool fstk_Break() {
if (contextStack.top().fileInfo->type != NODE_REPT) {
error("BREAK can only be used inside a REPT/FOR block\n");
return false;
}
fstk_StopRept();
contextStack.top().nbReptIters = 0; // Prevent more iterations
return true;
}

View File

@@ -77,6 +77,7 @@ struct FileUnmapDeleter {
static char *mapFile(int fd, std::string const &path, size_t size) {
void *mappingAddr = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
// LCOV_EXCL_START
if (mappingAddr == MAP_FAILED && errno == ENOTSUP) {
// The implementation may not support MAP_PRIVATE; try again with MAP_SHARED
// instead, offering, I believe, weaker guarantees about external modifications to
@@ -86,6 +87,7 @@ static char *mapFile(int fd, std::string const &path, size_t size) {
}
mappingAddr = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0);
}
// LCOV_EXCL_STOP
return mappingAddr != MAP_FAILED ? static_cast<char *>(mappingAddr) : nullptr;
}
@@ -134,11 +136,8 @@ struct CaseInsensitive {
}
};
// Identifiers that are also keywords are listed here. This ONLY applies to ones
// that would normally be matched as identifiers! Check out `yylex_NORMAL` to
// see how this is used.
// Tokens / keywords not handled here are handled in `yylex_NORMAL`'s switch.
// This assumes that no two keywords have the same name.
// This map lists all RGBASM keywords which `yylex_NORMAL` lexes as identifiers.
// All non-identifier tokens are lexed separately.
static std::unordered_map<std::string, int, CaseInsensitive, CaseInsensitive> keywordDict = {
{"ADC", T_(SM83_ADC) },
{"ADD", T_(SM83_ADD) },
@@ -241,20 +240,28 @@ static std::unordered_map<std::string, int, CaseInsensitive, CaseInsensitive> ke
{"BITWIDTH", T_(OP_BITWIDTH) },
{"TZCOUNT", T_(OP_TZCOUNT) },
{"STRCMP", T_(OP_STRCMP) },
{"STRIN", T_(OP_STRIN) },
{"STRRIN", T_(OP_STRRIN) },
{"STRSUB", T_(OP_STRSUB) },
{"STRLEN", T_(OP_STRLEN) },
{"STRCAT", T_(OP_STRCAT) },
{"STRUPR", T_(OP_STRUPR) },
{"STRLWR", T_(OP_STRLWR) },
{"STRRPL", T_(OP_STRRPL) },
{"STRCHAR", T_(OP_STRCHAR) },
{"STRCMP", T_(OP_STRCMP) },
{"STRFIND", T_(OP_STRFIND) },
{"STRFMT", T_(OP_STRFMT) },
{"STRIN", T_(OP_STRIN) },
{"STRLEN", T_(OP_STRLEN) },
{"STRLWR", T_(OP_STRLWR) },
{"STRRFIND", T_(OP_STRRFIND) },
{"STRRIN", T_(OP_STRRIN) },
{"STRRPL", T_(OP_STRRPL) },
{"STRSLICE", T_(OP_STRSLICE) },
{"STRSUB", T_(OP_STRSUB) },
{"STRUPR", T_(OP_STRUPR) },
{"CHARCMP", T_(OP_CHARCMP) },
{"CHARLEN", T_(OP_CHARLEN) },
{"CHARSIZE", T_(OP_CHARSIZE) },
{"CHARSUB", T_(OP_CHARSUB) },
{"CHARVAL", T_(OP_CHARVAL) },
{"INCHARMAP", T_(OP_INCHARMAP) },
{"REVCHAR", T_(OP_REVCHAR) },
{"INCLUDE", T_(POP_INCLUDE) },
{"PRINT", T_(POP_PRINT) },
@@ -407,21 +414,27 @@ bool LexerState::setFileAsNextState(std::string const &filePath, bool updateStat
if (filePath == "-") {
path = "<stdin>";
content.emplace<BufferedContent>(STDIN_FILENO);
// LCOV_EXCL_START
if (verbose) {
printf("Opening stdin\n");
}
// LCOV_EXCL_STOP
} else {
struct stat statBuf;
if (stat(filePath.c_str(), &statBuf) != 0) {
// LCOV_EXCL_START
error("Failed to stat file \"%s\": %s\n", filePath.c_str(), strerror(errno));
return false;
// LCOV_EXCL_STOP
}
path = filePath;
int fd = open(path.c_str(), O_RDONLY);
if (fd < 0) {
// LCOV_EXCL_START
error("Failed to open file \"%s\": %s\n", path.c_str(), strerror(errno));
return false;
// LCOV_EXCL_STOP
}
bool isMmapped = false;
@@ -433,9 +446,11 @@ bool LexerState::setFileAsNextState(std::string const &filePath, bool updateStat
content.emplace<ViewedContent>(
std::shared_ptr<char[]>(mappingAddr, FileUnmapDeleter(size)), size
);
// LCOV_EXCL_START
if (verbose) {
printf("File \"%s\" is mmap()ped\n", path.c_str());
}
// LCOV_EXCL_STOP
isMmapped = true;
}
}
@@ -443,6 +458,7 @@ bool LexerState::setFileAsNextState(std::string const &filePath, bool updateStat
if (!isMmapped) {
// Sometimes mmap() fails or isn't available, so have a fallback
content.emplace<BufferedContent>(fd);
// LCOV_EXCL_START
if (verbose) {
if (statBuf.st_size == 0) {
printf("File \"%s\" is empty\n", path.c_str());
@@ -452,6 +468,7 @@ bool LexerState::setFileAsNextState(std::string const &filePath, bool updateStat
);
}
}
// LCOV_EXCL_STOP
}
}
@@ -547,7 +564,9 @@ size_t BufferedContent::readMore(size_t startIndex, size_t nbChars) {
ssize_t nbReadChars = read(fd, &buf[startIndex], nbChars);
if (nbReadChars == -1) {
// LCOV_EXCL_START
fatalerror("Error while reading \"%s\": %s\n", lexerState->path.c_str(), strerror(errno));
// LCOV_EXCL_STOP
}
size += nbReadChars;
@@ -592,9 +611,7 @@ static bool isMacroChar(char c) {
// forward declarations for readBracketedMacroArgNum
static int peek();
static void shiftChar();
static uint32_t readNumber(int radix, uint32_t baseValue);
static bool startsIdentifier(int c);
static bool continuesIdentifier(int c);
static uint32_t readDecimalNumber(int initial);
static uint32_t readBracketedMacroArgNum() {
bool disableMacroArgs = lexerState->disableMacroArgs;
@@ -606,13 +623,24 @@ static uint32_t readBracketedMacroArgNum() {
lexerState->disableInterpolation = disableInterpolation;
}};
uint32_t num = 0;
int32_t num = 0;
int c = peek();
bool empty = false;
bool symbolError = false;
bool negative = c == '-';
if (negative) {
shiftChar();
c = peek();
}
if (c >= '0' && c <= '9') {
num = readNumber(10, 0);
uint32_t n = readDecimalNumber(0);
if (n > INT32_MAX) {
error("Number in bracketed macro argument is too large\n");
return 0;
}
num = negative ? -n : static_cast<int32_t>(n);
} else if (startsIdentifier(c) || c == '#') {
if (c == '#') {
shiftChar();
@@ -645,7 +673,7 @@ static uint32_t readBracketedMacroArgNum() {
num = 0;
symbolError = true;
} else {
num = sym->getConstantValue();
num = static_cast<int32_t>(sym->getConstantValue());
}
} else {
empty = true;
@@ -685,7 +713,7 @@ static std::shared_ptr<std::string> readMacroArg(char name) {
assume(str); // '\#' should always be defined (at least as an empty string)
return str;
} else if (name == '<') {
uint32_t num = readBracketedMacroArgNum();
int32_t num = readBracketedMacroArgNum();
if (num == 0) {
// The error was already reported by `readBracketedMacroArgNum`.
return nullptr;
@@ -699,7 +727,7 @@ static std::shared_ptr<std::string> readMacroArg(char name) {
auto str = macroArgs->getArg(num);
if (!str) {
error("Macro argument '\\<%" PRIu32 ">' not defined\n", num);
error("Macro argument '\\<%" PRId32 ">' not defined\n", num);
}
return str;
} else {
@@ -756,7 +784,9 @@ int LexerState::peekCharAhead() {
// and `.peekCharAhead()` will continue with its parent
assume(exp.offset <= exp.size());
if (exp.offset + distance < exp.size()) {
return static_cast<uint8_t>((*exp.contents)[exp.offset + distance]);
// Macro args can't be recursive, since `peek()` marks them as scanned, so
// this is a failsafe that (as far as I can tell) won't ever actually run.
return static_cast<uint8_t>((*exp.contents)[exp.offset + distance]); // LCOV_EXCL_LINE
}
distance -= exp.size() - exp.offset;
}
@@ -841,14 +871,14 @@ static void shiftChar() {
lexerState->macroArgScanDistance--;
restart:
for (;;) {
if (!lexerState->expansions.empty()) {
// Advance within the current expansion
if (Expansion &exp = lexerState->expansions.front(); exp.advance()) {
// When advancing would go past an expansion's end,
// move up to its parent and try again to advance
lexerState->expansions.pop_front();
goto restart;
continue;
}
} else {
// Advance within the file contents
@@ -858,6 +888,8 @@ restart:
lexerState->content.get<BufferedContent>().advance();
}
}
return;
}
}
static int nextChar() {
@@ -987,26 +1019,6 @@ static std::string readAnonLabelRef(char c) {
return sym_MakeAnonLabelName(n, c == '-');
}
static uint32_t readNumber(int radix, uint32_t baseValue) {
uint32_t value = baseValue;
for (;; shiftChar()) {
int c = peek();
if (c == '_') {
continue;
} else if (c < '0' || c > '0' + radix - 1) {
break;
}
if (value > (UINT32_MAX - (c - '0')) / radix) {
warning(WARNING_LARGE_CONSTANT, "Integer constant is too large\n");
}
value = value * radix + (c - '0');
}
return value;
}
static uint32_t readFractionalPart(uint32_t integer) {
uint32_t value = 0, divisor = 1;
uint8_t precision = 0;
@@ -1072,21 +1084,64 @@ static uint32_t readFractionalPart(uint32_t integer) {
}
char binDigits[2];
char gfxDigits[4];
static bool isValidDigit(char c) {
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '.'
|| c == '#' || c == '@';
}
static bool checkDigitErrors(char const *digits, size_t n, char const *type) {
for (size_t i = 0; i < n; i++) {
char c = digits[i];
if (!isValidDigit(c)) {
error("Invalid digit for %s constant %s\n", type, printChar(c));
return false;
}
if (c >= '0' && c < static_cast<char>(n + '0') && c != static_cast<char>(i + '0')) {
error("Changed digit for %s constant %s\n", type, printChar(c));
return false;
}
for (size_t j = i + 1; j < n; j++) {
if (c == digits[j]) {
error("Repeated digit for %s constant %s\n", type, printChar(c));
return false;
}
}
}
return true;
}
void lexer_SetBinDigits(char const digits[2]) {
if (size_t n = std::size(binDigits); checkDigitErrors(digits, n, "binary")) {
memcpy(binDigits, digits, n);
}
}
void lexer_SetGfxDigits(char const digits[4]) {
if (size_t n = std::size(gfxDigits); checkDigitErrors(digits, n, "graphics")) {
memcpy(gfxDigits, digits, n);
}
}
static uint32_t readBinaryNumber() {
uint32_t value = 0;
bool empty = true;
for (;; shiftChar()) {
int c = peek();
int bit;
// Check for '_' after digits in case one of the digits is '_'
if (c == binDigits[0]) {
bit = 0;
} else if (c == binDigits[1]) {
bit = 1;
} else if (c == '_') {
if (c == '_' && !empty) {
continue;
} else if (c == '0' || c == binDigits[0]) {
bit = 0;
} else if (c == '1' || c == binDigits[1]) {
bit = 1;
} else {
break;
}
@@ -1094,6 +1149,72 @@ static uint32_t readBinaryNumber() {
warning(WARNING_LARGE_CONSTANT, "Integer constant is too large\n");
}
value = value * 2 + bit;
empty = false;
}
if (empty) {
error("Invalid integer constant, no digits after '%%'\n");
}
return value;
}
static uint32_t readOctalNumber() {
uint32_t value = 0;
bool empty = true;
for (;; shiftChar()) {
int c = peek();
if (c == '_' && !empty) {
continue;
} else if (c >= '0' && c <= '7') {
c = c - '0';
} else {
break;
}
if (value > (UINT32_MAX - c) / 8) {
warning(WARNING_LARGE_CONSTANT, "Integer constant is too large\n");
}
value = value * 8 + c;
empty = false;
}
if (empty) {
error("Invalid integer constant, no digits after '&'\n");
}
return value;
}
static uint32_t readDecimalNumber(int initial) {
uint32_t value = initial ? initial - '0' : 0;
bool empty = !initial;
for (;; shiftChar()) {
int c = peek();
if (c == '_' && !empty) {
continue;
} else if (c >= '0' && c <= '9') {
c = c - '0';
} else {
break;
}
if (value > (UINT32_MAX - c) / 10) {
warning(WARNING_LARGE_CONSTANT, "Integer constant is too large\n");
}
value = value * 10 + c;
empty = false;
}
if (empty) {
error("Invalid integer constant, no digits\n");
}
return value;
@@ -1106,14 +1227,14 @@ static uint32_t readHexNumber() {
for (;; shiftChar()) {
int c = peek();
if (c >= 'a' && c <= 'f') {
if (c == '_' && !empty) {
continue;
} else if (c >= 'a' && c <= 'f') {
c = c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
c = c - 'A' + 10;
} else if (c >= '0' && c <= '9') {
c = c - '0';
} else if (c == '_' && !empty) {
continue;
} else {
break;
}
@@ -1133,8 +1254,6 @@ static uint32_t readHexNumber() {
return value;
}
char gfxDigits[4];
static uint32_t readGfxConstant() {
uint32_t bitPlaneLower = 0, bitPlaneUpper = 0;
uint8_t width = 0;
@@ -1143,17 +1262,16 @@ static uint32_t readGfxConstant() {
int c = peek();
uint32_t pixel;
// Check for '_' after digits in case one of the digits is '_'
if (c == gfxDigits[0]) {
pixel = 0;
} else if (c == gfxDigits[1]) {
pixel = 1;
} else if (c == gfxDigits[2]) {
pixel = 2;
} else if (c == gfxDigits[3]) {
pixel = 3;
} else if (c == '_' && width > 0) {
if (c == '_' && width > 0) {
continue;
} else if (c == '0' || c == gfxDigits[0]) {
pixel = 0;
} else if (c == '1' || c == gfxDigits[1]) {
pixel = 1;
} else if (c == '2' || c == gfxDigits[2]) {
pixel = 2;
} else if (c == '3' || c == gfxDigits[3]) {
pixel = 3;
} else {
break;
}
@@ -1179,31 +1297,22 @@ static uint32_t readGfxConstant() {
return bitPlaneUpper << 8 | bitPlaneLower;
}
// Functions to read identifiers & keywords
static bool startsIdentifier(int c) {
// Anonymous labels internally start with '!'
return (c <= 'Z' && c >= 'A') || (c <= 'z' && c >= 'a') || c == '.' || c == '_';
}
static bool continuesIdentifier(int c) {
return startsIdentifier(c) || (c <= '9' && c >= '0') || c == '#' || c == '$' || c == '@';
}
// Functions to read identifiers and keywords
static Token readIdentifier(char firstChar, bool raw) {
std::string identifier(1, firstChar);
int tokenType = firstChar == '.' ? T_(LOCAL_ID) : T_(ID);
int tokenType = firstChar == '.' ? T_(LOCAL) : T_(SYMBOL);
// Continue reading while the char is in the symbol charset
// Continue reading while the char is in the identifier charset
for (int c = peek(); continuesIdentifier(c); c = peek()) {
shiftChar();
// Write the char to the identifier's name
identifier += c;
// If the char was a dot, mark the identifier as local
// If the char was a dot, the identifier is a local label
if (c == '.') {
tokenType = T_(LOCAL_ID);
tokenType = T_(LOCAL);
}
}
@@ -1219,7 +1328,7 @@ static Token readIdentifier(char firstChar, bool raw) {
// Label scopes `.` and `..` are the only nonlocal identifiers that start with a dot
if (identifier.find_first_not_of('.') == identifier.npos) {
tokenType = T_(ID);
tokenType = T_(SYMBOL);
}
return Token(tokenType, identifier);
@@ -1276,7 +1385,7 @@ static std::shared_ptr<std::string> readInterpolation(size_t depth) {
lexerState->disableInterpolation = disableInterpolation;
if (fmtBuf.starts_with('#')) {
// Skip a '#' raw identifier prefix, but after expanding any nested interpolations.
// Skip a '#' raw symbol prefix, but after expanding any nested interpolations.
fmtBuf.erase(0, 1);
} else if (keywordDict.find(fmtBuf) != keywordDict.end()) {
// Don't allow symbols that alias keywords without a '#' prefix.
@@ -1318,8 +1427,11 @@ static void appendEscapedString(std::string &str, std::string const &escape) {
str += "\\n";
break;
case '\r':
// A literal CR in a string may get treated as a LF, so '\r' is not tested.
// LCOV_EXCL_START
str += "\\r";
break;
// LCOV_EXCL_STOP
case '\t':
str += "\\t";
break;
@@ -1620,6 +1732,12 @@ static void appendStringLiteral(std::string &str, bool raw) {
static Token yylex_SKIP_TO_ENDC(); // forward declaration for yylex_NORMAL
// Must stay in sync with the `switch` in `yylex_NORMAL`!
static bool isGarbageCharacter(int c) {
return c != EOF && !continuesIdentifier(c)
&& (c == '\0' || !strchr("; \t~[](),+-*/|^=!<>:&%`\"\r\n\\", c));
}
static Token yylex_NORMAL() {
for (;;) {
int c = nextChar();
@@ -1641,7 +1759,7 @@ static Token yylex_NORMAL() {
case '@': {
std::string symName("@");
return Token(T_(ID), symName);
return Token(T_(SYMBOL), symName);
}
case '[':
@@ -1657,12 +1775,17 @@ static Token yylex_NORMAL() {
// Handle ambiguous 1- or 2-char tokens
case '+': // Either += or ADD
if (peek() == '=') {
case '+': // Either +=, ADD, or CAT
switch (peek()) {
case '=':
shiftChar();
return Token(T_(POP_ADDEQ));
}
case '+':
shiftChar();
return Token(T_(OP_CAT));
default:
return Token(T_(OP_ADD));
}
case '-': // Either -= or SUB
if (peek() == '=') {
@@ -1795,7 +1918,7 @@ static Token yylex_NORMAL() {
case 'o':
case 'O':
shiftChar();
return Token(T_(NUMBER), readNumber(8, 0));
return Token(T_(NUMBER), readOctalNumber());
case 'b':
case 'B':
shiftChar();
@@ -1814,7 +1937,7 @@ static Token yylex_NORMAL() {
case '7':
case '8':
case '9': {
uint32_t n = readNumber(10, c - '0');
uint32_t n = readDecimalNumber(c);
if (peek() == '.') {
shiftChar();
@@ -1832,7 +1955,7 @@ static Token yylex_NORMAL() {
shiftChar();
return Token(T_(OP_LOGICAND));
} else if (c >= '0' && c <= '7') {
return Token(T_(NUMBER), readNumber(8, 0));
return Token(T_(NUMBER), readOctalNumber());
}
return Token(T_(OP_AND));
@@ -1841,7 +1964,7 @@ static Token yylex_NORMAL() {
if (c == '=') {
shiftChar();
return Token(T_(POP_MODEQ));
} else if (c == binDigits[0] || c == binDigits[1]) {
} else if (c == '0' || c == '1' || c == binDigits[0] || c == binDigits[1]) {
return Token(T_(NUMBER), readBinaryNumber());
}
return Token(T_(OP_MOD));
@@ -1903,15 +2026,15 @@ static Token yylex_NORMAL() {
}
// If a keyword, don't try to expand
if (token.type != T_(ID) && token.type != T_(LOCAL_ID)) {
if (token.type != T_(SYMBOL) && token.type != T_(LOCAL)) {
return token;
}
// `token` is either an `ID` or a `LOCAL_ID`, and both have a `std::string` value.
// `token` is either a `SYMBOL` or a `LOCAL`, and both have a `std::string` value.
assume(token.value.holds<std::string>());
// Local symbols cannot be string expansions
if (token.type == T_(ID) && lexerState->expandStrings) {
// Raw symbols and local symbols cannot be string expansions
if (!raw && token.type == T_(SYMBOL) && lexerState->expandStrings) {
// Attempt string expansion
Symbol const *sym = sym_FindExactSymbol(token.value.get<std::string>());
@@ -1925,18 +2048,18 @@ static Token yylex_NORMAL() {
}
// This is a "lexer hack"! We need it to distinguish between label definitions
// (which start with `LABEL`) and macro invocations (which start with `ID`).
// (which start with `LABEL`) and macro invocations (which start with `SYMBOL`).
//
// If we had one `IDENTIFIER` token, the parser would need to perform "lookahead"
// to determine which rule applies. But since macros need to enter "raw" mode to
// parse their arguments, which may not even be valid tokens in "normal" mode, we
// cannot use lookahead to check for the presence of a `COLON`.
//
// Instead, we have separate `ID` and `LABEL` tokens, lexing as a `LABEL` if a ':'
// character *immediately* follows the identifier. Thus, at the beginning of a line,
// "Label:" and "mac:" are treated as label definitions, but "Label :" and "mac :"
// are treated as macro invocations.
if (token.type == T_(ID) && peek() == ':') {
// Instead, we have separate `SYMBOL` and `LABEL` tokens, lexing as a `LABEL` if a
// ':' character *immediately* follows the identifier. Thus, at the beginning of a
// line, "Label:" and "mac:" are treated as label definitions, but "Label :" and
// "mac :" are treated as macro invocations.
if (token.type == T_(SYMBOL) && peek() == ':') {
token.type = T_(LABEL);
}
@@ -1945,10 +2068,21 @@ static Token yylex_NORMAL() {
// Do not report weird characters when capturing, it'll be done later
if (!lexerState->capturing) {
// TODO: try to group reportings
assume(isGarbageCharacter(c) || c == '#');
if (isGarbageCharacter(peek())) {
// At least two characters are garbage; group them into one error report
std::string garbage = printChar(c);
while (isGarbageCharacter(peek())) {
c = nextChar();
garbage += ", ";
garbage += printChar(c);
}
error("Unknown characters %s\n", garbage.c_str());
} else {
error("Unknown character %s\n", printChar(c));
}
}
}
lexerState->atLineStart = false;
}
}
@@ -2090,7 +2224,7 @@ append:
}
}
finish:
finish: // Can't `break` out of a nested `for`-`switch`
// Trim right whitespace
auto rightPos = std::find_if_not(str.rbegin(), str.rend(), isWhitespace);
str.resize(rightPos.base() - str.begin());
@@ -2158,7 +2292,8 @@ static Token skipIfBlock(bool toEndc) {
case T_(POP_ELIF):
if (lexer_ReachedELSEBlock()) {
fatalerror("Found ELIF after an ELSE block\n");
// This should be redundant, as the parser handles this error first.
fatalerror("Found ELIF after an ELSE block\n"); // LCOV_EXCL_LINE
}
if (!toEndc && lexer_GetIFDepth() == startingDepth) {
return token;
@@ -2250,9 +2385,8 @@ static Token yylex_SKIP_TO_ENDR() {
case T_(POP_ENDR):
depth--;
if (!depth) {
return Token(T_(YYEOF)); // yywrap() will finish the REPT/FOR loop
}
// `lexer_CaptureRept` has already guaranteed that the `ENDR`s are balanced
assume(depth > 0);
break;
case T_(POP_IF):
@@ -2390,7 +2524,7 @@ Capture lexer_CaptureRept() {
do { // Discard initial whitespace
c = nextChar();
} while (isWhitespace(c));
// Now, try to match `REPT`, `FOR` or `ENDR` as a **whole** identifier
// Now, try to match `REPT`, `FOR` or `ENDR` as a **whole** keyword
if (startsIdentifier(c)) {
switch (readIdentifier(c, false).type) {
case T_(POP_REPT):
@@ -2443,7 +2577,7 @@ Capture lexer_CaptureMacro() {
do { // Discard initial whitespace
c = nextChar();
} while (isWhitespace(c));
// Now, try to match `ENDM` as a **whole** identifier
// Now, try to match `ENDM` as a **whole** keyword
if (startsIdentifier(c)) {
switch (readIdentifier(c, false).type) {
case T_(POP_ENDM):

View File

@@ -8,10 +8,16 @@
#include "asm/warning.hpp"
std::shared_ptr<std::string> MacroArgs::getArg(uint32_t i) const {
uint32_t realIndex = i + shift - 1;
std::shared_ptr<std::string> MacroArgs::getArg(int32_t i) const {
// Bracketed macro arguments adjust negative indexes such that -1 is the last argument.
if (i < 0) {
i += args.size() + 1;
}
return realIndex >= args.size() ? nullptr : args[realIndex];
int32_t realIndex = i + shift - 1;
return realIndex < 0 || static_cast<uint32_t>(realIndex) >= args.size() ? nullptr
: args[realIndex];
}
std::shared_ptr<std::string> MacroArgs::getAllArgs() const {

View File

@@ -36,7 +36,7 @@ static std::string make_escape(std::string &str) {
size_t pos = 0;
for (;;) {
// All dollars needs to be doubled
size_t nextPos = str.find("$", pos);
size_t nextPos = str.find('$', pos);
if (nextPos == std::string::npos) {
break;
}
@@ -87,6 +87,7 @@ static option const longopts[] = {
{nullptr, no_argument, nullptr, 0 }
};
// LCOV_EXCL_START
static void printUsage() {
fputs(
"Usage: rgbasm [-EhVvw] [-b chars] [-D name[=value]] [-g chars] [-I path]\n"
@@ -107,6 +108,7 @@ static void printUsage() {
stderr
);
}
// LCOV_EXCL_STOP
int main(int argc, char *argv[]) {
time_t now = time(nullptr);
@@ -176,8 +178,10 @@ int main(int argc, char *argv[]) {
break;
case 'h':
// LCOV_EXCL_START
printUsage();
exit(0);
// LCOV_EXCL_STOP
case 'I':
fstk_AddIncludePath(musl_optarg);
@@ -195,7 +199,7 @@ int main(int argc, char *argv[]) {
dependFileName = "<stdout>";
}
if (dependFile == nullptr) {
err("Failed to open dependfile \"%s\"", dependFileName);
err("Failed to open dependfile \"%s\"", dependFileName); // LCOV_EXCL_LINE
}
break;
@@ -302,9 +306,11 @@ int main(int argc, char *argv[]) {
if (stateFileSpecs.find(name) != stateFileSpecs.end()) {
warnx("Overriding state filename %s", name);
}
// LCOV_EXCL_START
if (verbose) {
printf("State filename %s\n", name);
}
// LCOV_EXCL_STOP
stateFileSpecs.emplace(name, std::move(features));
break;
}
@@ -314,8 +320,10 @@ int main(int argc, char *argv[]) {
exit(0);
case 'v':
// LCOV_EXCL_START
verbose = true;
break;
// LCOV_EXCL_STOP
case 'W':
opt_W(musl_optarg);
@@ -367,8 +375,10 @@ int main(int argc, char *argv[]) {
// Unrecognized options
default:
// LCOV_EXCL_START
printUsage();
exit(1);
// LCOV_EXCL_STOP
}
}
@@ -391,7 +401,7 @@ int main(int argc, char *argv[]) {
std::string mainFileName = argv[musl_optind];
if (verbose) {
printf("Assembling %s\n", mainFileName.c_str());
printf("Assembling %s\n", mainFileName.c_str()); // LCOV_EXCL_LINE
}
if (dependFile) {

View File

@@ -14,8 +14,8 @@
#include "asm/warning.hpp"
struct OptStackEntry {
char binary[2];
char gbgfx[4];
char binDigits[2];
char gfxDigits[4];
uint8_t fixPrecision;
uint8_t fillByte;
bool warningsAreErrors;
@@ -151,13 +151,8 @@ void opt_Push() {
OptStackEntry entry;
// Both of these are pulled from lexer.hpp
entry.binary[0] = binDigits[0];
entry.binary[1] = binDigits[1];
entry.gbgfx[0] = gfxDigits[0];
entry.gbgfx[1] = gfxDigits[1];
entry.gbgfx[2] = gfxDigits[2];
entry.gbgfx[3] = gfxDigits[3];
memcpy(entry.binDigits, binDigits, std::size(binDigits));
memcpy(entry.gfxDigits, gfxDigits, std::size(gfxDigits));
entry.fixPrecision = fixPrecision; // Pulled from fixpoint.hpp
@@ -181,8 +176,8 @@ void opt_Pop() {
OptStackEntry entry = stack.top();
stack.pop();
opt_B(entry.binary);
opt_G(entry.gbgfx);
opt_B(entry.binDigits);
opt_G(entry.gfxDigits);
opt_P(entry.fillByte);
opt_Q(entry.fixPrecision);
opt_R(entry.maxRecursionDepth);

View File

@@ -61,7 +61,7 @@ void out_RegisterNode(std::shared_ptr<FileStackNode> node) {
}
}
// Return a section's ID, or UINT32_MAX if the section is not in the list
// Return a section's ID, or UINT32_MAX if the section does not exist
static uint32_t getSectIDIfAny(Section *sect) {
if (!sect) {
return UINT32_MAX;
@@ -71,7 +71,8 @@ static uint32_t getSectIDIfAny(Section *sect) {
return static_cast<uint32_t>(search->second);
}
fatalerror("Unknown section '%s'\n", sect->name.c_str());
// Every section that exists should be in `sectionMap`
fatalerror("Unknown section '%s'\n", sect->name.c_str()); // LCOV_EXCL_LINE
}
static void writePatch(Patch const &patch, FILE *file) {
@@ -175,7 +176,7 @@ static void writeRpn(std::vector<uint8_t> &rpnexpr, std::vector<uint8_t> const &
sym = sym_FindExactSymbol(symName);
if (sym->isConstant()) {
rpnexpr[rpnptr++] = RPN_CONST;
value = sym_GetConstantValue(symName);
value = sym->getConstantValue();
} else {
rpnexpr[rpnptr++] = RPN_SYM;
registerUnregisteredSymbol(*sym); // Ensure that `sym->ID` is set
@@ -323,7 +324,7 @@ void out_WriteObject() {
file = stdout;
}
if (!file) {
err("Failed to open object file '%s'", objectFileName.c_str());
err("Failed to open object file '%s'", objectFileName.c_str()); // LCOV_EXCL_LINE
}
Defer closeFile{[&] { fclose(file); }};
@@ -343,14 +344,7 @@ void out_WriteObject() {
writeFileStackNode(node, file);
// The list is supposed to have decrementing IDs
if (it + 1 != fileStackNodes.end() && it[1]->ID != node.ID - 1) {
fatalerror(
"Internal error: fstack node #%" PRIu32 " follows #%" PRIu32
". Please report this to the developers!\n",
it[1]->ID,
node.ID
);
}
assume(it + 1 == fileStackNodes.end() || it[1]->ID == node.ID - 1);
}
for (Symbol const *sym : objectSymbols) {
@@ -373,9 +367,11 @@ void out_SetFileName(std::string const &name) {
warnx("Overriding output filename %s", objectFileName.c_str());
}
objectFileName = name;
// LCOV_EXCL_START
if (verbose) {
printf("Output filename %s\n", objectFileName.c_str());
}
// LCOV_EXCL_STOP
}
static void dumpString(std::string const &escape, FILE *file) {
@@ -528,7 +524,7 @@ void out_WriteState(std::string name, std::vector<StateFeature> const &features)
file = stdout;
}
if (!file) {
err("Failed to open state file '%s'", name.c_str());
err("Failed to open state file '%s'", name.c_str()); // LCOV_EXCL_LINE
}
Defer closeFile{[&] { fclose(file); }};

File diff suppressed because it is too large Load Diff

View File

@@ -75,6 +75,9 @@ void Expression::makeSymbol(std::string const &symName) {
if (Symbol *sym = sym_FindScopedSymbol(symName); sym_IsPC(sym) && !sect_GetSymbolSection()) {
error("PC has no value outside of a section\n");
data = 0;
} else if (sym && !sym->isNumeric() && !sym->isLabel()) {
error("'%s' is not a numeric symbol\n", symName.c_str());
data = 0;
} else if (!sym || !sym->isConstant()) {
isSymbol = true;
@@ -91,7 +94,7 @@ void Expression::makeSymbol(std::string const &symName) {
*ptr++ = RPN_SYM;
memcpy(ptr, sym->name.c_str(), nameLen);
} else {
data = static_cast<int32_t>(sym_GetConstantValue(symName));
data = static_cast<int32_t>(sym->getConstantValue());
}
}
@@ -322,40 +325,12 @@ void Expression::makeUnaryOp(RPNCommand op, Expression &&src) {
case RPN_TZCOUNT:
data = val != 0 ? ctz(uval) : 32;
break;
case RPN_LOGOR:
case RPN_LOGAND:
case RPN_LOGEQ:
case RPN_LOGGT:
case RPN_LOGLT:
case RPN_LOGGE:
case RPN_LOGLE:
case RPN_LOGNE:
case RPN_ADD:
case RPN_SUB:
case RPN_XOR:
case RPN_OR:
case RPN_AND:
case RPN_SHL:
case RPN_SHR:
case RPN_USHR:
case RPN_MUL:
case RPN_DIV:
case RPN_MOD:
case RPN_EXP:
case RPN_BANK_SYM:
case RPN_BANK_SECT:
case RPN_BANK_SELF:
case RPN_SIZEOF_SECT:
case RPN_STARTOF_SECT:
case RPN_SIZEOF_SECTTYPE:
case RPN_STARTOF_SECTTYPE:
case RPN_HRAM:
case RPN_RST:
case RPN_CONST:
case RPN_SYM:
fatalerror("%d is not an unary operator\n", op);
default:
// `makeUnaryOp` should never be called with a non-unary operator!
// LCOV_EXCL_START
unreachable_();
}
// LCOV_EXCL_STOP
} else if (op == RPN_LOGNOT && tryConstLogNot(src)) {
data = 0;
} else if (int32_t constVal; op == RPN_LOW && (constVal = tryConstLow(src)) != -1) {
@@ -498,27 +473,12 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
data = op_exponent(lval, rval);
break;
case RPN_NEG:
case RPN_NOT:
case RPN_LOGNOT:
case RPN_BANK_SYM:
case RPN_BANK_SECT:
case RPN_BANK_SELF:
case RPN_SIZEOF_SECT:
case RPN_STARTOF_SECT:
case RPN_SIZEOF_SECTTYPE:
case RPN_STARTOF_SECTTYPE:
case RPN_HRAM:
case RPN_RST:
case RPN_HIGH:
case RPN_LOW:
case RPN_BITWIDTH:
case RPN_TZCOUNT:
case RPN_CONST:
case RPN_SYM:
fatalerror("%d is not a binary operator\n", op);
default:
// `makeBinaryOp` should never be called with a non-binary operator!
// LCOV_EXCL_START
unreachable_();
}
// LCOV_EXCL_STOP
} else if (op == RPN_SUB && src1.isDiffConstant(src2.symbolOf())) {
data = src1.symbolOf()->getValue() - src2.symbolOf()->getValue();
} else if ((op == RPN_LOGAND || op == RPN_AND) && tryConstZero(src1, src2)) {
@@ -605,10 +565,24 @@ void Expression::makeCheckRST() {
}
}
void Expression::makeCheckBitIndex(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;
} 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"};
error("Invalid bit index %" PRId32 " for %s\n", val, instructions[mask >> 6]);
}
}
// Checks that an RPN expression's value fits within N bits (signed or unsigned)
void Expression::checkNBit(uint8_t n) const {
if (isKnown()) {
::checkNBit(value(), n, "Expression");
::checkNBit(value(), n, nullptr);
}
}
@@ -617,11 +591,23 @@ bool checkNBit(int32_t v, uint8_t n, char const *name) {
assume(n < CHAR_BIT * sizeof(int)); // Otherwise `1 << n` is UB
if (v < -(1 << n) || v >= 1 << n) {
warning(WARNING_TRUNCATION_1, "%s must be %u-bit\n", name, n);
warning(
WARNING_TRUNCATION_1,
n == 8 && !name ? "%s must be %u-bit; use LOW() to force 8-bit\n"
: "%s must be %u-bit\n",
name ? name : "Expression",
n
);
return false;
}
if (v < -(1 << (n - 1))) {
warning(WARNING_TRUNCATION_2, "%s must be %u-bit\n", name, n);
warning(
WARNING_TRUNCATION_2,
n == 8 && !name ? "%s must be %u-bit; use LOW() to force 8-bit\n"
: "%s must be %u-bit\n",
name ? name : "Expression",
n
);
return false;
}

View File

@@ -768,7 +768,7 @@ void sect_Skip(uint32_t skip, bool ds) {
}
// Output a byte that can be relocatable or constant
void sect_RelByte(Expression &expr, uint32_t pcShift) {
void sect_RelByte(Expression const &expr, uint32_t pcShift) {
if (!requireCodeSection()) {
return;
}
@@ -782,15 +782,13 @@ void sect_RelByte(Expression &expr, uint32_t pcShift) {
}
// Output several bytes that can be relocatable or constant
void sect_RelBytes(uint32_t n, std::vector<Expression> &exprs) {
void sect_RelBytes(uint32_t n, std::vector<Expression> const &exprs) {
if (!requireCodeSection()) {
return;
}
for (uint32_t i = 0; i < n; i++) {
Expression &expr = exprs[i % exprs.size()];
if (!expr.isKnown()) {
if (Expression const &expr = exprs[i % exprs.size()]; !expr.isKnown()) {
createPatch(PATCHTYPE_BYTE, expr, i);
writeByte(0);
} else {
@@ -800,7 +798,7 @@ void sect_RelBytes(uint32_t n, std::vector<Expression> &exprs) {
}
// Output a word that can be relocatable or constant
void sect_RelWord(Expression &expr, uint32_t pcShift) {
void sect_RelWord(Expression const &expr, uint32_t pcShift) {
if (!requireCodeSection()) {
return;
}
@@ -814,7 +812,7 @@ void sect_RelWord(Expression &expr, uint32_t pcShift) {
}
// Output a long that can be relocatable or constant
void sect_RelLong(Expression &expr, uint32_t pcShift) {
void sect_RelLong(Expression const &expr, uint32_t pcShift) {
if (!requireCodeSection()) {
return;
}
@@ -828,7 +826,7 @@ void sect_RelLong(Expression &expr, uint32_t pcShift) {
}
// Output a PC-relative byte that can be relocatable or constant
void sect_PCRelByte(Expression &expr, uint32_t pcShift) {
void sect_PCRelByte(Expression const &expr, uint32_t pcShift) {
if (!requireCodeSection()) {
return;
}
@@ -877,9 +875,11 @@ void sect_BinaryFile(std::string const &name, int32_t startPos) {
}
if (!file) {
if (generatedMissingIncludes) {
// LCOV_EXCL_START
if (verbose) {
printf("Aborting (-MG) on INCBIN file '%s' (%s)\n", name.c_str(), strerror(errno));
}
// LCOV_EXCL_STOP
failedOnMissingInclude = true;
} else {
error("Error opening INCBIN file '%s': %s\n", name.c_str(), strerror(errno));
@@ -944,9 +944,11 @@ void sect_BinaryFileSlice(std::string const &name, int32_t startPos, int32_t len
}
if (!file) {
if (generatedMissingIncludes) {
// LCOV_EXCL_START
if (verbose) {
printf("Aborting (-MG) on INCBIN file '%s' (%s)\n", name.c_str(), strerror(errno));
}
// LCOV_EXCL_STOP
failedOnMissingInclude = true;
} else {
error("Error opening INCBIN file '%s': %s\n", name.c_str(), strerror(errno));

View File

@@ -2,6 +2,7 @@
#include "asm/symbol.hpp"
#include <algorithm>
#include <inttypes.h>
#include <stdio.h>
#include <unordered_map>
@@ -9,6 +10,7 @@
#include "error.hpp"
#include "helpers.hpp" // assume
#include "util.hpp"
#include "version.hpp"
#include "asm/fstack.hpp"
@@ -130,6 +132,11 @@ static void updateSymbolFilename(Symbol &sym) {
}
}
static bool isValidIdentifier(std::string const &s) {
return !s.empty() && startsIdentifier(s[0])
&& std::all_of(s.begin() + 1, s.end(), [](char c) { return continuesIdentifier(c); });
}
static void alreadyDefinedError(Symbol const &sym, char const *asType) {
if (sym.isBuiltin && !sym_FindScopedValidSymbol(sym.name)) {
// `DEF()` would return false, so we should not claim the symbol is already defined
@@ -141,6 +148,15 @@ static void alreadyDefinedError(Symbol const &sym, char const *asType) {
}
fputs(" at ", stderr);
dumpFilename(sym);
if (sym.type == SYM_EQUS) {
if (std::string const &contents = *sym.getEqus(); isValidIdentifier(contents)) {
fprintf(
stderr,
" (should it be {interpolated} to define its contents \"%s\"?)\n",
contents.c_str()
);
}
}
}
}
@@ -325,19 +341,6 @@ uint32_t Symbol::getConstantValue() const {
return 0;
}
uint32_t sym_GetConstantValue(std::string const &symName) {
if (Symbol const *sym = sym_FindScopedSymbol(symName); sym) {
return sym->getConstantValue();
}
if (sym_IsPurgedScoped(symName)) {
error("'%s' not defined; it was purged\n", symName.c_str());
} else {
error("'%s' not defined\n", symName.c_str());
}
return 0;
}
std::pair<Symbol const *, Symbol const *> sym_GetCurrentLabelScopes() {
return {globalScope, localScope};
}
@@ -528,8 +531,10 @@ static uint32_t anonLabelID = 0;
Symbol *sym_AddAnonLabel() {
if (anonLabelID == UINT32_MAX) {
// LCOV_EXCL_START
error("Only %" PRIu32 " anonymous labels can be created!", anonLabelID);
return nullptr;
// LCOV_EXCL_STOP
}
std::string anon = sym_MakeAnonLabelName(0, true); // The direction is important!
@@ -555,6 +560,7 @@ std::string sym_MakeAnonLabelName(uint32_t ofs, bool neg) {
} else {
ofs--; // We're referencing symbols that haven't been created yet...
if (ofs > UINT32_MAX - anonLabelID) {
// LCOV_EXCL_START
error(
"Reference to anonymous label %" PRIu32 " after, when only %" PRIu32
" may still be created\n",
@@ -562,19 +568,21 @@ std::string sym_MakeAnonLabelName(uint32_t ofs, bool neg) {
UINT32_MAX - anonLabelID
);
} else {
// LCOV_EXCL_STOP
id = anonLabelID + ofs;
}
}
std::string anon("!");
anon += std::to_string(id);
return anon;
return "!"s + std::to_string(id);
}
void sym_Export(std::string const &symName) {
if (symName.starts_with('!')) {
// LCOV_EXCL_START
// The parser does not accept anonymous labels for an `EXPORT` directive
error("Anonymous labels cannot be exported\n");
return;
// LCOV_EXCL_STOP
}
Symbol *sym = sym_FindScopedSymbol(symName);
@@ -656,11 +664,13 @@ void sym_Init(time_t now) {
sym_AddEqu("__RGBDS_RC__"s, PACKAGE_VERSION_RC)->isBuiltin = true;
#endif
// LCOV_EXCL_START
if (now == static_cast<time_t>(-1)) {
warn("Failed to determine current time");
// Fall back by pretending we are at the Epoch
now = 0;
}
// LCOV_EXCL_STOP
tm const *time_local = localtime(&now);

View File

@@ -12,17 +12,20 @@ if [ "$BISON_MAJOR" -lt 3 ]; then
exit 1
fi
BISON_FLAGS="-Wall -Dparse.lac=full -Dlr.type=ielr"
BISON_FLAGS="-Wall -Dlr.type=ielr"
# Set some optimization flags on versions that support them
if [ "$BISON_MAJOR" -eq 4 ] || [ "$BISON_MAJOR" -eq 3 ] && [ "$BISON_MINOR" -ge 5 ]; then
BISON_FLAGS="$BISON_FLAGS -Dapi.token.raw=true"
if [ "$BISON_MAJOR" -ge 4 ] || [ "$BISON_MAJOR" -eq 3 ] && [ "$BISON_MINOR" -ge 5 ]; then
BISON_FLAGS="$BISON_FLAGS -Dparse.lac=full -Dapi.token.raw=true"
fi
if [ "$BISON_MAJOR" -eq 4 ] || [ "$BISON_MAJOR" -eq 3 ] && [ "$BISON_MINOR" -ge 6 ]; then
if [ "$BISON_MAJOR" -ge 4 ] || [ "$BISON_MAJOR" -eq 3 ] && [ "$BISON_MINOR" -ge 6 ]; then
BISON_FLAGS="$BISON_FLAGS -Dparse.error=detailed"
else
BISON_FLAGS="$BISON_FLAGS -Dparse.error=verbose"
fi
if [ "$BISON_MAJOR" -ge 4 ] || [ "$BISON_MAJOR" -eq 3 ] && [ "$BISON_MINOR" -ge 7 ]; then
BISON_FLAGS="$BISON_FLAGS -Wcounterexamples"
fi
# Replace the arguments to this script ($@) with the ones in $BISON_FLAGS
eval "set -- $BISON_FLAGS"

View File

@@ -23,7 +23,7 @@ static_assert(UNSPECIFIED > 0xFF, "UNSPECIFIED should not be in byte range!");
static constexpr off_t BANK_SIZE = 0x4000;
// Short options
static char const *optstring = "Ccf:hi:jk:L:l:m:n:Op:r:st:Vv";
static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:Vv";
// Equivalent long options
// Please keep in the same order as short opts.
@@ -45,6 +45,7 @@ static option const longopts[] = {
{"mbc-type", required_argument, nullptr, 'm'},
{"rom-version", required_argument, nullptr, 'n'},
{"overwrite", no_argument, nullptr, 'O'},
{"output", required_argument, nullptr, 'o'},
{"pad-value", required_argument, nullptr, 'p'},
{"ram-size", required_argument, nullptr, 'r'},
{"sgb-compatible", no_argument, nullptr, 's'},
@@ -54,6 +55,7 @@ static option const longopts[] = {
{nullptr, no_argument, nullptr, 0 }
};
// LCOV_EXCL_START
static void printUsage() {
fputs(
"Usage: rgbfix [-hjOsVv] [-C | -c] [-f <fix_spec>] [-i <game_id>] [-k <licensee>]\n"
@@ -65,6 +67,7 @@ static void printUsage() {
" to the man page for a list of values\n"
" -p, --pad-value <value> pad to the next valid size using this value\n"
" -r, --ram-size <code> set the cart RAM size byte to this value\n"
" -o, --output <path> set the output file\n"
" -V, --version print RGBFIX version and exit\n"
" -v, --validate fix the header logo and both checksums (-f lhg)\n"
"\n"
@@ -72,6 +75,7 @@ static void printUsage() {
stderr
);
}
// LCOV_EXCL_STOP
static uint8_t nbErrors;
@@ -696,10 +700,12 @@ static char const *mbcName(MbcType type) {
case MBC_WRONG_FEATURES:
case MBC_BAD_RANGE:
case MBC_BAD_TPP1:
// LCOV_EXCL_START
unreachable_();
}
unreachable_();
// LCOV_EXCL_STOP
}
static bool hasRAM(MbcType type) {
@@ -713,8 +719,7 @@ static bool hasRAM(MbcType type) {
case MBC3_TIMER_BATTERY:
case MBC5:
case MBC5_RUMBLE:
case MBC6: // TODO: not sure
case BANDAI_TAMA5: // TODO: not sure
case BANDAI_TAMA5: // "Game de Hakken!! Tamagotchi - Osutchi to Mesutchi" has RAM size 0
case MBC_NONE:
case MBC_BAD:
case MBC_WRONG_FEATURES:
@@ -735,6 +740,7 @@ static bool hasRAM(MbcType type) {
case MBC5_RAM_BATTERY:
case MBC5_RUMBLE_RAM:
case MBC5_RUMBLE_RAM_BATTERY:
case MBC6: // "Net de Get - Minigame @ 100" has RAM size 3 (32 KiB)
case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
case POCKET_CAMERA:
case HUC3:
@@ -761,7 +767,7 @@ static bool hasRAM(MbcType type) {
break;
}
unreachable_();
unreachable_(); // LCOV_EXCL_LINE
}
static uint8_t const nintendoLogo[] = {
@@ -811,8 +817,9 @@ static ssize_t readBytes(int fd, uint8_t *buf, size_t len) {
while (len) {
ssize_t ret = read(fd, buf, len);
if (ret == -1 && errno != EINTR) { // Return errors, unless we only were interrupted
return -1;
// Return errors, unless we only were interrupted
if (ret == -1 && errno != EINTR) {
return -1; // LCOV_EXCL_LINE
}
// EOF reached
if (ret == 0) {
@@ -838,8 +845,9 @@ static ssize_t writeBytes(int fd, uint8_t *buf, size_t len) {
while (len) {
ssize_t ret = write(fd, buf, len);
if (ret == -1 && errno != EINTR) { // Return errors, unless we only were interrupted
return -1;
// Return errors, unless we only were interrupted
if (ret == -1 && errno != EINTR) {
return -1; // LCOV_EXCL_LINE
}
// If anything was written, accumulate it, and continue
if (ret != -1) {
@@ -879,9 +887,9 @@ static void overwriteBytes(
memcpy(&rom0[startAddr], fixed, size);
}
static void processFile(int input, int output, char const *name, off_t fileSize) {
// Both of these should be true for seekable files, and neither otherwise
if (input == output) {
static void
processFile(int input, int output, char const *name, off_t fileSize, bool expectFileSize) {
if (expectFileSize) {
assume(fileSize != 0);
} else {
assume(fileSize == 0);
@@ -893,8 +901,10 @@ static void processFile(int input, int output, char const *name, off_t fileSize)
ssize_t headerSize = (cartridgeType & 0xFF00) == TPP1 ? 0x154 : 0x150;
if (rom0Len == -1) {
// LCOV_EXCL_START
report("FATAL: Failed to read \"%s\"'s header: %s\n", name, strerror(errno));
return;
// LCOV_EXCL_STOP
} else if (rom0Len < headerSize) {
report(
"FATAL: \"%s\" too short, expected at least %jd ($%jx) bytes, got only %jd\n",
@@ -1132,8 +1142,10 @@ static void processFile(int input, int output, char const *name, off_t fileSize)
// write the header
if (input == output) {
if (lseek(output, 0, SEEK_SET) == static_cast<off_t>(-1)) {
// LCOV_EXCL_START
report("FATAL: Failed to rewind \"%s\": %s\n", name, strerror(errno));
return;
// LCOV_EXCL_STOP
}
// If modifying the file in-place, we only need to edit the header
// However, padding may have modified ROM0 (added padding), so don't in that case
@@ -1144,9 +1156,12 @@ static void processFile(int input, int output, char const *name, off_t fileSize)
writeLen = writeBytes(output, rom0, rom0Len);
if (writeLen == -1) {
// LCOV_EXCL_START
report("FATAL: Failed to write \"%s\"'s ROM0: %s\n", name, strerror(errno));
return;
// LCOV_EXCL_STOP
} else if (writeLen < rom0Len) {
// LCOV_EXCL_START
report(
"FATAL: Could only write %jd of \"%s\"'s %jd ROM0 bytes\n",
static_cast<intmax_t>(writeLen),
@@ -1154,6 +1169,7 @@ static void processFile(int input, int output, char const *name, off_t fileSize)
static_cast<intmax_t>(rom0Len)
);
return;
// LCOV_EXCL_STOP
}
// Output ROMX if it was buffered
@@ -1162,9 +1178,12 @@ static void processFile(int input, int output, char const *name, off_t fileSize)
// so it's fine to cast to `size_t`
writeLen = writeBytes(output, romx.data(), totalRomxLen);
if (writeLen == -1) {
// LCOV_EXCL_START
report("FATAL: Failed to write \"%s\"'s ROMX: %s\n", name, strerror(errno));
return;
// LCOV_EXCL_STOP
} else if (static_cast<size_t>(writeLen) < totalRomxLen) {
// LCOV_EXCL_START
report(
"FATAL: Could only write %jd of \"%s\"'s %zu ROMX bytes\n",
static_cast<intmax_t>(writeLen),
@@ -1172,6 +1191,7 @@ static void processFile(int input, int output, char const *name, off_t fileSize)
totalRomxLen
);
return;
// LCOV_EXCL_STOP
}
}
@@ -1179,8 +1199,10 @@ static void processFile(int input, int output, char const *name, off_t fileSize)
if (padValue != UNSPECIFIED) {
if (input == output) {
if (lseek(output, 0, SEEK_END) == static_cast<off_t>(-1)) {
// LCOV_EXCL_START
report("FATAL: Failed to seek to end of \"%s\": %s\n", name, strerror(errno));
return;
// LCOV_EXCL_STOP
}
}
memset(bank, padValue, sizeof(bank));
@@ -1194,40 +1216,73 @@ static void processFile(int input, int output, char const *name, off_t fileSize)
// The return value is either -1, or at most `thisLen`,
// so it's fine to cast to `size_t`
if (static_cast<size_t>(ret) != thisLen) {
// LCOV_EXCL_START
report("FATAL: Failed to write \"%s\"'s padding: %s\n", name, strerror(errno));
break;
// LCOV_EXCL_STOP
}
len -= thisLen;
}
}
}
static bool processFilename(char const *name) {
static bool processFilename(char const *name, char const *outputName) {
nbErrors = 0;
if (!strcmp(name, "-")) {
bool inputStdin = !strcmp(name, "-");
if (inputStdin && !outputName) {
outputName = "-";
}
int output = -1;
bool openedOutput = false;
if (outputName) {
if (!strcmp(outputName, "-")) {
output = STDOUT_FILENO;
(void)setmode(STDOUT_FILENO, O_BINARY);
} else {
output = open(outputName, O_WRONLY | O_BINARY | O_CREAT, 0600);
if (output == -1) {
report(
"FATAL: Failed to open \"%s\" for writing: %s\n", outputName, strerror(errno)
);
return true;
}
openedOutput = true;
}
}
Defer closeOutput{[&] {
if (openedOutput) {
close(output);
}
}};
if (inputStdin) {
name = "<stdin>";
(void)setmode(STDIN_FILENO, O_BINARY);
(void)setmode(STDOUT_FILENO, O_BINARY);
processFile(STDIN_FILENO, STDOUT_FILENO, name, 0);
processFile(STDIN_FILENO, output, name, 0, false);
} else {
// POSIX specifies that the results of O_RDWR on a FIFO are undefined.
// However, this is necessary to avoid a TOCTTOU, if the file was changed between
// `stat()` and `open(O_RDWR)`, which could trigger the UB anyway.
// Thus, we're going to hope that either the `open` fails, or it succeeds but IO
// operations may fail, all of which we handle.
if (int input = open(name, O_RDWR | O_BINARY); input == -1) {
if (int input = open(name, (outputName ? O_RDONLY : O_RDWR) | O_BINARY); input == -1) {
report("FATAL: Failed to open \"%s\" for reading+writing: %s\n", name, strerror(errno));
} else {
Defer closeInput{[&] { close(input); }};
struct stat stat;
if (fstat(input, &stat) == -1) {
// LCOV_EXCL_START
report("FATAL: Failed to stat \"%s\": %s\n", name, strerror(errno));
} else if (!S_ISREG(stat.st_mode)) { // TODO: Do we want to support other types?
// LCOV_EXCL_STOP
} else if (!S_ISREG(stat.st_mode)) { // We do not support FIFOs or symlinks
// LCOV_EXCL_START
report(
"FATAL: \"%s\" is not a regular file, and thus cannot be modified in-place\n",
name
);
// LCOV_EXCL_STOP
} else if (stat.st_size < 0x150) {
// This check is in theory redundant with the one in `processFile`, but it
// prevents passing a file size of 0, which usually indicates pipes
@@ -1237,7 +1292,10 @@ static bool processFilename(char const *name) {
static_cast<intmax_t>(stat.st_size)
);
} else {
processFile(input, input, name, stat.st_size);
if (!outputName) {
output = input;
}
processFile(input, output, name, stat.st_size, true);
}
}
}
@@ -1281,6 +1339,7 @@ static void parseByte(uint16_t &output, char name) {
int main(int argc, char *argv[]) {
nbErrors = 0;
char const *outputFilename = nullptr;
for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) {
switch (ch) {
size_t len;
@@ -1322,8 +1381,10 @@ int main(int argc, char *argv[]) {
break;
case 'h':
// LCOV_EXCL_START
printUsage();
exit(0);
// LCOV_EXCL_STOP
case 'i':
gameID = musl_optarg;
@@ -1393,6 +1454,10 @@ int main(int argc, char *argv[]) {
overwriteRom = true;
break;
case 'o':
outputFilename = musl_optarg;
break;
case 'p':
parseByte(padValue, 'p');
break;
@@ -1419,16 +1484,20 @@ int main(int argc, char *argv[]) {
}
case 'V':
// LCOV_EXCL_START
printf("rgbfix %s\n", get_package_version_string());
exit(0);
// LCOV_EXCL_STOP
case 'v':
fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM;
break;
default:
// LCOV_EXCL_START
printUsage();
exit(1);
// LCOV_EXCL_STOP
}
}
@@ -1462,7 +1531,7 @@ int main(int argc, char *argv[]) {
"warning: RAM size 1 (2 KiB) was specified for MBC \"%s\"\n",
mbcName(cartridgeType)
);
} // TODO: check possible values?
}
} else if (ramSize) {
fprintf(
stderr,
@@ -1543,8 +1612,14 @@ int main(int argc, char *argv[]) {
exit(1);
}
if (outputFilename && argc != musl_optind + 1) {
fputs("FATAL: If `-o` is set then only a single input file may be specified\n", stderr);
printUsage();
exit(1);
}
do {
failed |= processFilename(*argv);
failed |= processFilename(*argv, outputFilename);
} while (*++argv);
return failed;

View File

@@ -12,12 +12,10 @@
#include <stdlib.h>
#include <string.h>
#include <string_view>
#include <type_traits>
#include <vector>
#include "extern/getopt.hpp"
#include "file.hpp"
#include "helpers.hpp" // assume
#include "platform.hpp"
#include "version.hpp"
@@ -103,6 +101,7 @@ void fatal(char const *fmt, ...) {
}
void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
// LCOV_EXCL_START
if (verbosity >= level) {
va_list ap;
@@ -110,10 +109,11 @@ void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
vfprintf(stderr, fmt, ap);
va_end(ap);
}
// LCOV_EXCL_STOP
}
// Short options
static char const *optstring = "-Aa:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ";
static char const *optstring = "-Aa:B:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ";
// Equivalent long options
// Please keep in the same order as short opts.
@@ -125,6 +125,7 @@ static char const *optstring = "-Aa:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ"
static option const longopts[] = {
{"auto-attr-map", no_argument, nullptr, 'A'},
{"attr-map", required_argument, nullptr, 'a'},
{"background-color", required_argument, nullptr, 'B'},
{"base-tiles", required_argument, nullptr, 'b'},
{"color-curve", no_argument, nullptr, 'C'},
{"colors", required_argument, nullptr, 'c'},
@@ -156,6 +157,7 @@ static option const longopts[] = {
{nullptr, no_argument, nullptr, 0 }
};
// LCOV_EXCL_START
static void printUsage() {
fputs(
"Usage: rgbgfx [-r stride] [-ChmOuVXYZ] [-v [-v ...]] [-a <attr_map> | -A]\n"
@@ -174,6 +176,7 @@ static void printUsage() {
stderr
);
}
// LCOV_EXCL_STOP
// Parses a number at the beginning of a string, moving the pointer to skip the parsed characters.
// Returns the provided errVal on error.
@@ -279,8 +282,8 @@ static std::vector<size_t> readAtFile(std::string const &path, std::vector<char>
// We only filter out `EOF`, but calling `isblank()` on anything else is UB!
static_assert(
std::remove_reference_t<decltype(*file)>::traits_type::eof() == EOF,
"isblank(char_traits<...>::eof()) is UB!"
std::streambuf::traits_type::eof() == EOF,
"isblank(std::streambuf::traits_type::eof()) is UB!"
);
std::vector<size_t> argvOfs;
@@ -361,6 +364,9 @@ static char *parseArgv(int argc, char *argv[]) {
}
options.attrmap = musl_optarg;
break;
case 'B':
parseBackgroundPalSpec(musl_optarg);
break;
case 'b':
number = parseNumber(arg, "Bank 0 base tile ID", 0);
if (number >= 256) {
@@ -400,18 +406,18 @@ static char *parseArgv(int argc, char *argv[]) {
options.useColorCurve = true;
break;
case 'c':
localOptions.externalPalSpec = nullptr; // Allow overriding a previous pal spec
if (musl_optarg[0] == '#') {
options.palSpecType = Options::EXPLICIT;
parseInlinePalSpec(musl_optarg);
} else if (strcasecmp(musl_optarg, "embedded") == 0) {
// Use PLTE, error out if missing
options.palSpecType = Options::EMBEDDED;
} else if (strncasecmp(musl_optarg, "dmg=", literal_strlen("dmg=")) == 0) {
options.palSpecType = Options::DMG;
parseDmgPalSpec(&musl_optarg[literal_strlen("dmg=")]);
} else {
options.palSpecType = Options::EXPLICIT;
// Can't parse the file yet, as "flat" color collections need to know the palette
// size to be split; thus, we defer that
// TODO: this does not validate the `fmt` part of any external spec but the last
// one, but I guess that's okay
localOptions.externalPalSpec = musl_optarg;
}
break;
@@ -425,8 +431,10 @@ static char *parseArgv(int argc, char *argv[]) {
}
break;
case 'h':
// LCOV_EXCL_START
printUsage();
exit(0);
// LCOV_EXCL_STOP
case 'i':
if (!options.inputTileset.empty()) {
warning("Overriding input tileset file %s", options.inputTileset.c_str());
@@ -582,13 +590,17 @@ static char *parseArgv(int argc, char *argv[]) {
options.tilemap = musl_optarg;
break;
case 'V':
// LCOV_EXCL_START
printf("rgbgfx %s\n", get_package_version_string());
exit(0);
// LCOV_EXCL_STOP
case 'v':
// LCOV_EXCL_START
if (options.verbosity < Options::VERB_VVVVVV) {
++options.verbosity;
}
break;
// LCOV_EXCL_STOP
case 'x':
options.trim = parseNumber(arg, "Number of tiles to trim", 0);
if (*arg != '\0') {
@@ -615,8 +627,10 @@ static char *parseArgv(int argc, char *argv[]) {
}
break;
default:
// LCOV_EXCL_START
printUsage();
exit(1);
// LCOV_EXCL_STOP
}
}
@@ -744,15 +758,37 @@ int main(int argc, char *argv[]) {
parseExternalPalSpec(localOptions.externalPalSpec);
}
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_CFG) {
fprintf(stderr, "rgbgfx %s\n", get_package_version_string());
if (options.verbosity >= Options::VERB_VVVVVV) {
putc('\n', stderr);
// clang-format off: vertically align values
static std::array<uint16_t, 21> gfx{
0x1FE, 0x3FF, 0x399, 0x399, 0x3FF, 0x3FF, 0x381, 0x3C3, 0x1FE, 0x078, 0x1FE,
0x3FF, 0x3FF, 0x3FF, 0x37B, 0x37B, 0x0FC, 0x0CC, 0x1CE, 0x1CE, 0x1CE,
0b0111111110,
0b1111111111,
0b1110011001,
0b1110011001,
0b1111111111,
0b1111111111,
0b1110000001,
0b1111000011,
0b0111111110,
0b0001111000,
0b0111111110,
0b1111111111,
0b1111111111,
0b1111111111,
0b1101111011,
0b1101111011,
0b0011111100,
0b0011001100,
0b0111001110,
0b0111001110,
0b0111001110,
};
// clang-format on
static std::array<char const *, 3> textbox{
" ,----------------------------------------.",
" | Augh, dimensional interference again?! |",
@@ -805,6 +841,8 @@ int main(int argc, char *argv[]) {
return "Explicit";
case Options::EMBEDDED:
return "Embedded";
case Options::DMG:
return "DMG";
}
return "???";
}());
@@ -825,7 +863,7 @@ int main(int argc, char *argv[]) {
}
fprintf(
stderr,
"\tInput image slice: %" PRIu32 "x%" PRIu32 " pixels starting at (%" PRIi32 ", %" PRIi32
"\tInput image slice: %" PRIu32 "x%" PRIu32 " pixels starting at (%" PRId32 ", %" PRId32
")\n",
options.inputSlice.width,
options.inputSlice.height,
@@ -856,6 +894,7 @@ int main(int argc, char *argv[]) {
printPath("Output palettes", options.palettes);
fputs("Ready.\n", stderr);
}
// LCOV_EXCL_STOP
// Do not do anything if option parsing went wrong.
requireZeroErrors();

View File

@@ -86,8 +86,8 @@ private:
public:
Iter() = default;
bool operator==(Iter const &other) const { return _iter == other._iter; }
bool operator!=(Iter const &other) const { return !(*this == other); }
bool operator==(Iter const &rhs) const { return _iter == rhs._iter; }
Iter &operator++() {
++_iter;
skipEmpty();
@@ -98,6 +98,7 @@ private:
++(*this);
return it;
}
reference operator*() const {
assume((*_iter).has_value());
return **_iter;
@@ -167,16 +168,6 @@ private:
std::unordered_set<uint16_t> &uniqueColors() const {
// We check for *distinct* colors by stuffing them into a `set`; this should be
// faster than "back-checking" on every element (O(n²))
//
// TODO: calc84maniac suggested another approach; try implementing it, see if it
// performs better:
// > So basically you make a priority queue that takes iterators into each of your sets
// > (paired with end iterators so you'll know where to stop), and the comparator tests the
// > values pointed to by each iterator
// > Then each iteration you pop from the queue,
// > optionally add one to your count, increment the iterator and push it back into the
// > queue if it didn't reach the end
// > And you do this until the priority queue is empty
static std::unordered_set<uint16_t> colors;
colors.clear();
@@ -308,7 +299,7 @@ static void decant(
break;
}
auto attrs = from.begin();
std::advance(attrs, (iter - processed.begin()));
std::advance(attrs, iter - processed.begin());
// Build up the "component"...
colors.clear();
@@ -476,7 +467,6 @@ std::tuple<DefaultInitVec<size_t>, size_t>
);
// All efficiencies are identical iff min equals max
// TODO: maybe not ideal to re-compute these two?
ProtoPalette const &minProtoPal = protoPalettes[minEfficiencyIter->protoPalIndex];
ProtoPalette const &maxProtoPal = protoPalettes[maxEfficiencyIter->protoPalIndex];
size_t minSize = minProtoPal.size();
@@ -558,6 +548,7 @@ std::tuple<DefaultInitVec<size_t>, size_t>
}
}
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
for (auto &&assignment : assignments) {
fprintf(stderr, "{ ");
@@ -570,11 +561,13 @@ std::tuple<DefaultInitVec<size_t>, size_t>
fprintf(stderr, "} (volume = %zu)\n", assignment.volume());
}
}
// LCOV_EXCL_STOP
// "Decant" the result
decant(assignments, protoPalettes);
// Note that the result does not contain any empty palettes
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
for (auto &&assignment : assignments) {
fprintf(stderr, "{ ");
@@ -587,6 +580,7 @@ std::tuple<DefaultInitVec<size_t>, size_t>
fprintf(stderr, "} (volume = %zu)\n", assignment.volume());
}
}
// LCOV_EXCL_STOP
DefaultInitVec<size_t> mappings(protoPalettes.size());
for (size_t i = 0; i < assignments.size(); ++i) {

View File

@@ -40,7 +40,7 @@ void sortIndexed(
return true;
}
}
unreachable_(); // This should not be possible
unreachable_(); // LCOV_EXCL_LINE
});
}
}

View File

@@ -23,7 +23,12 @@
using namespace std::string_view_literals;
constexpr uint8_t nibble(char c) {
template<typename Str> // Should be std::string or std::string_view
static void skipWhitespace(Str 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;
@@ -36,27 +41,21 @@ constexpr uint8_t nibble(char c) {
}
}
constexpr uint8_t toHex(char c1, char c2) {
static constexpr uint8_t toHex(char c1, char c2) {
return nibble(c1) * 16 + nibble(c2);
}
constexpr uint8_t singleToHex(char c) {
static constexpr uint8_t singleToHex(char c) {
return toHex(c, c);
}
template<typename Str> // Should be std::string or std::string_view
static void skipWhitespace(Str const &str, typename Str::size_type &pos) {
pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length());
}
void parseInlinePalSpec(char const * const rawArg) {
// List of #rrggbb/#rgb colors (or #none); comma-separated.
// Palettes are separated by colons.
std::string_view arg(rawArg);
using size_type = decltype(arg)::size_type;
auto parseError = [&rawArg, &arg](size_type ofs, size_type len, char const *msg) {
auto parseError = [&rawArg, &arg](size_t ofs, size_t len, char const *msg) {
(void)arg; // With NDEBUG, `arg` is otherwise not used
assume(ofs <= arg.length());
assume(len <= arg.length());
@@ -80,17 +79,16 @@ void parseInlinePalSpec(char const * const rawArg) {
options.palSpec.clear();
options.palSpec.emplace_back(); // Value-initialized, not default-init'd, so we get zeros
size_type n = 0; // Index into the argument
// TODO: store max `nbColors` ever reached, and compare against palette size later
size_t n = 0; // Index into the argument
size_t nbColors = 0; // Number of colors in the current palette
for (;;) {
++n; // Ignore the '#' (checked either by caller or previous loop iteration)
std::optional<Rgba> &color = options.palSpec.back()[nbColors];
// Check for #none first.
if (arg.compare(n, 4, "none"sv) == 0 || arg.compare(n, 4, "NONE"sv) == 0) {
// Check for "#none" first.
if (strncasecmp(&rawArg[n], "none", literal_strlen("none")) == 0) {
color = {};
n += 4;
n += literal_strlen("none");
} else {
auto pos = std::min(arg.find_first_not_of("0123456789ABCDEFabcdef"sv, n), arg.length());
switch (pos - n) {
@@ -194,7 +192,6 @@ static T readLE(U const *bytes) {
[[gnu::warn_unused_result]]
static bool readLine(std::filebuf &file, std::string &buffer) {
assume(buffer.empty());
// TODO: maybe this can be optimized to bulk reads?
for (;;) {
auto c = file.sbumpc();
if (c == std::filebuf::traits_type::eof()) {
@@ -222,7 +219,7 @@ static bool readLine(std::filebuf &file, std::string &buffer) {
// Parses the initial part of a string_view, advancing the "read index" as it does
template<typename U> // Should be uint*_t
static std::optional<U> parseDec(std::string const &str, std::string::size_type &n) {
static std::optional<U> parseDec(std::string const &str, size_t &n) {
uintmax_t value = 0;
auto result = std::from_chars(str.data() + n, str.data() + str.size(), value);
if (static_cast<bool>(result.ec)) {
@@ -232,8 +229,7 @@ static std::optional<U> parseDec(std::string const &str, std::string::size_type
return std::optional<U>{value};
}
static std::optional<Rgba>
parseColor(std::string const &str, std::string::size_type &n, uint16_t i) {
static std::optional<Rgba> parseColor(std::string const &str, size_t &n, uint16_t i) {
std::optional<uint8_t> r = parseDec<uint8_t>(str, n);
if (!r) {
error("Failed to parse color #%d (\"%s\"): invalid red component", i + 1, str.c_str());
@@ -281,7 +277,7 @@ static void parsePSPFile(std::filebuf &file) {
line.clear();
requireLine("PSP", file, line);
std::string::size_type n = 0;
size_t n = 0;
std::optional<uint16_t> nbColors = parseDec<uint16_t>(line, n);
if (!nbColors || n != line.length()) {
error("Invalid \"number of colors\" line in PSP file (%s)", line.c_str());
@@ -347,7 +343,7 @@ static void parseGPLFile(std::filebuf &file) {
continue;
}
std::string::size_type n = 0;
size_t n = 0;
skipWhitespace(line, n);
// Skip empty lines, or lines that contain just a comment.
if (line.length() == n || line[n] == '#') {
@@ -438,7 +434,6 @@ static void parseACTFile(std::filebuf &file) {
uint16_t nbColors = 256;
if (len == 772) {
nbColors = readBE<uint16_t>(&buf[768]);
// TODO: apparently there is a "transparent color index"? What?
if (nbColors > 256 || nbColors == 0) {
error("Invalid number of colors in ACT file (%" PRIu16 ")", nbColors);
return;
@@ -549,9 +544,6 @@ static void parseACOFile(std::filebuf &file) {
return;
}
}
// TODO: maybe scan the v2 data instead (if present)
// `codecvt` can be used to convert from UTF-16 to UTF-8
}
static void parseGBCFile(std::filebuf &file) {
@@ -603,8 +595,7 @@ void parseExternalPalSpec(char const *arg) {
std::tuple{"GBC", &parseGBCFile, std::ios::binary},
};
auto iter =
std::find_if(RANGE(parsers), [&arg, &ptr](decltype(parsers)::value_type const &parser) {
auto iter = std::find_if(RANGE(parsers), [&arg, &ptr](auto const &parser) {
return strncasecmp(arg, std::get<0>(parser), ptr - arg) == 0;
});
if (iter == parsers.end()) {
@@ -625,3 +616,61 @@ void parseExternalPalSpec(char const *arg) {
std::get<1> (*iter)(file);
}
void parseDmgPalSpec(char const * const rawArg) {
// Two hex digit DMG palette spec
std::string_view arg(rawArg);
if (arg.length() != 2
|| arg.find_first_not_of("0123456789ABCDEFabcdef"sv) != std::string_view::npos) {
error("Unknown DMG palette specification \"%s\"", rawArg);
return;
}
options.palSpecDmg = toHex(arg[0], arg[1]);
// Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex`
for (uint8_t i = 0; i < 4; ++i) {
options.dmgColors[options.dmgValue(i)] = i;
}
// Validate that DMG palette spec does not have conflicting colors
for (uint8_t i = 0; i < 3; ++i) {
for (uint8_t j = i + 1; j < 4; ++j) {
if (options.dmgValue(i) == options.dmgValue(j)) {
error("DMG palette specification maps two gray shades to the same color index");
return;
}
}
}
}
void parseBackgroundPalSpec(char const *arg) {
if (strcasecmp(arg, "transparent") == 0) {
options.bgColor = Rgba(0x00, 0x00, 0x00, 0x00);
return;
}
if (arg[0] != '#') {
error("Background color specification must be `#rgb`, `#rrggbb`, or `transparent`");
return;
}
size_t size = strspn(&arg[1], "0123456789ABCDEFabcdef");
switch (size) {
case 3:
options.bgColor = Rgba(singleToHex(arg[1]), singleToHex(arg[2]), singleToHex(arg[3]), 0xFF);
break;
case 6:
options.bgColor =
Rgba(toHex(arg[1], arg[2]), toHex(arg[3], arg[4]), toHex(arg[5], arg[6]), 0xFF);
break;
default:
error("Unknown background color specification \"%s\"", arg);
}
if (arg[size + 1] != '\0') {
error("Unexpected text \"%s\" after background color specification", &arg[size + 1]);
}
}

View File

@@ -25,6 +25,10 @@
#include "gfx/pal_sorting.hpp"
#include "gfx/proto_palette.hpp"
static bool isBgColorTransparent() {
return options.bgColor.has_value() && options.bgColor->isTransparent();
}
class ImagePalette {
std::array<std::optional<Rgba>, NB_COLOR_SLOTS> _colors;
@@ -36,9 +40,9 @@ public:
// color), then the other color is returned. Otherwise, `nullptr` is returned.
[[nodiscard]]
Rgba const *registerColor(Rgba const &rgba) {
decltype(_colors)::value_type &slot = _colors[rgba.cgbColor()];
std::optional<Rgba> &slot = _colors[rgba.cgbColor()];
if (rgba.cgbColor() == Rgba::transparent) {
if (rgba.cgbColor() == Rgba::transparent && !isBgColorTransparent()) {
options.hasTransparentPixels = true;
}
@@ -52,7 +56,7 @@ public:
}
size_t size() const {
return std::count_if(RANGE(_colors), [](decltype(_colors)::value_type const &slot) {
return std::count_if(RANGE(_colors), [](std::optional<Rgba> const &slot) {
return slot.has_value() && !slot->isTransparent();
});
}
@@ -193,23 +197,20 @@ public:
PNG_LIBPNG_VER_STRING, static_cast<png_voidp>(this), handleError, handleWarning
);
if (!png) {
fatal("Failed to allocate PNG structure: %s", strerror(errno));
fatal("Failed to create PNG read structure: %s", strerror(errno)); // LCOV_EXCL_LINE
}
info = png_create_info_struct(png);
if (!info) {
// LCOV_EXCL_START
png_destroy_read_struct(&png, nullptr, nullptr);
fatal("Failed to allocate PNG info structure: %s", strerror(errno));
fatal("Failed to create PNG info structure: %s", strerror(errno));
// LCOV_EXCL_STOP
}
png_set_read_fn(png, this, readData);
png_set_sig_bytes(png, pngHeader.size());
// TODO: png_set_crc_action(png, PNG_CRC_ERROR_QUIT, PNG_CRC_WARN_DISCARD);
// Skipping chunks we don't use should improve performance
// TODO: png_set_keep_unknown_chunks(png, ...);
// Process all chunks up to but not including the image data
png_read_info(png, info);
@@ -288,9 +289,7 @@ public:
options.verbosePrint(Options::VERB_INTERM, "No embedded palette\n");
}
// Set up transformations; to turn everything into RGBA888
// TODO: it's not necessary to uniformize the pixel data (in theory), and not doing
// so *might* improve performance, and should reduce memory usage.
// Set up transformations to turn everything into RGBA888 for simplicity of handling
// Convert grayscale to RGB
switch (colorType & ~PNG_COLOR_MASK_ALPHA) {
@@ -466,7 +465,6 @@ public:
}
bool operator==(Iterator const &rhs) const { return coords() == rhs.coords(); }
bool operator!=(Iterator const &rhs) const { return coords() != rhs.coords(); }
};
public:
@@ -518,10 +516,12 @@ struct AttrmapEntry {
bool yFlip;
bool xFlip;
static constexpr decltype(protoPaletteID) transparent = SIZE_MAX;
static constexpr size_t transparent = static_cast<size_t>(-1);
static constexpr size_t background = static_cast<size_t>(-2);
bool isBackgroundTile() const { return protoPaletteID == background; }
size_t getPalID(DefaultInitVec<size_t> const &mappings) const {
return protoPaletteID == transparent ? 0 : mappings[protoPaletteID];
return mappings[isBackgroundTile() || protoPaletteID == transparent ? 0 : protoPaletteID];
}
};
@@ -552,10 +552,10 @@ static void generatePalSpec(Png const &png) {
static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
generatePalettes(std::vector<ProtoPalette> const &protoPalettes, Png const &png) {
// Run a "pagination" problem solver
// TODO: allow picking one of several solvers?
auto [mappings, nbPalettes] = overloadAndRemove(protoPalettes);
assume(mappings.size() == protoPalettes.size());
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
fprintf(
stderr,
@@ -567,6 +567,7 @@ static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
fprintf(stderr, "%zu -> %zu\n", i, mappings[i]);
}
}
// LCOV_EXCL_STOP
std::vector<Palette> palettes(nbPalettes);
// If the image contains at least one transparent pixel, force transparency in the first slot of
@@ -585,8 +586,10 @@ static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
}
// "Sort" colors in the generated palettes, see the man page for the flowchart
auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal();
if (embPalRGB != nullptr) {
if (options.palSpecType == Options::DMG) {
sortGrayscale(palettes, png.getColors().raw());
} else if (auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal();
embPalRGB != nullptr) {
sortIndexed(palettes, embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha);
} else if (png.isSuitableForGrayscale()) {
sortGrayscale(palettes, png.getColors().raw());
@@ -601,9 +604,9 @@ static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
// Convert the palette spec to actual palettes
std::vector<Palette> palettes(options.palSpec.size());
for (auto [spec, pal] : zip(options.palSpec, palettes)) {
for (size_t i = 0; i < options.nbColorsPerPal && (!spec[i] || spec[i]->isOpaque()); ++i) {
for (size_t i = 0; i < options.nbColorsPerPal; ++i) {
// If the spec has a gap, there's no need to copy anything.
if (spec[i]) {
if (spec[i].has_value() && !spec[i]->isTransparent()) {
pal[i] = spec[i]->cgbColor();
}
}
@@ -654,6 +657,7 @@ static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
}
static void outputPalettes(std::vector<Palette> const &palettes) {
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
for (auto &&palette : palettes) {
fputs("{ ", stderr);
@@ -663,6 +667,7 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
fputs("}\n", stderr);
}
}
// LCOV_EXCL_STOP
if (palettes.size() > options.nbPalettes) {
// If the palette generation is wrong, other (dependee) operations are likely to be
@@ -677,7 +682,9 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
if (!options.palettes.empty()) {
File output;
if (!output.open(options.palettes, std::ios_base::out | std::ios_base::binary)) {
// LCOV_EXCL_START
fatal("Failed to create \"%s\": %s", output.c_str(options.palettes), strerror(errno));
// LCOV_EXCL_STOP
}
for (Palette const &palette : palettes) {
@@ -830,7 +837,9 @@ static void outputUnoptimizedTileData(
) {
File output;
if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) {
// LCOV_EXCL_START
fatal("Failed to create \"%s\": %s", output.c_str(options.output), strerror(errno));
// LCOV_EXCL_STOP
}
uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : png.getWidth() / 8;
@@ -843,7 +852,9 @@ static void outputUnoptimizedTileData(
remainingTiles -= options.trim;
for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
// If the tile is fully transparent, default to palette 0
// Do not emit fully-background tiles.
if (!attr.isBackgroundTile()) {
// If the tile is fully transparent, this defaults to palette 0.
Palette const &palette = palettes[attr.getPalID(mappings)];
for (uint32_t y = 0; y < 8; ++y) {
uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y);
@@ -852,6 +863,7 @@ static void outputUnoptimizedTileData(
output->sputc(bitplanes >> 8);
}
}
}
--remainingTiles;
if (remainingTiles == 0) {
@@ -869,7 +881,9 @@ static void outputUnoptimizedMaps(
if (!path.empty()) {
file.emplace();
if (!file->open(path, std::ios_base::out | std::ios_base::binary)) {
// LCOV_EXCL_START
fatal("Failed to create \"%s\": %s", file->c_str(options.tilemap), strerror(errno));
// LCOV_EXCL_STOP
}
}
};
@@ -887,18 +901,23 @@ static void outputUnoptimizedMaps(
}
if (tilemapOutput.has_value()) {
(*tilemapOutput)->sputc(tileID + options.baseTileIDs[bank]);
(*tilemapOutput)
->sputc((attr.isBackgroundTile() ? 0 : tileID) + options.baseTileIDs[bank]);
}
uint8_t palID = attr.getPalID(mappings);
if (attrmapOutput.has_value()) {
uint8_t palID = attr.getPalID(mappings) & 7;
(*attrmapOutput)->sputc(palID | bank << 3); // The other flags are all 0
(*attrmapOutput)->sputc((palID & 7) | bank << 3); // The other flags are all 0
}
if (palmapOutput.has_value()) {
(*palmapOutput)->sputc(attr.getPalID(mappings));
(*palmapOutput)->sputc(palID);
}
// Background tiles are skipped in the tile data, so they should be skipped in the maps too.
if (!attr.isBackgroundTile()) {
++tileID;
}
}
}
struct UniqueTiles {
std::unordered_set<TileData> tileset;
@@ -989,7 +1008,14 @@ static UniqueTiles dedupTiles(
bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty();
for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
auto [tileID, matchType] = tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]});
if (attr.isBackgroundTile()) {
attr.xFlip = false;
attr.yFlip = false;
attr.bank = 0;
attr.tileID = 0;
} else {
auto [tileID, matchType] =
tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]});
if (inputWithoutOutput && matchType == TileData::NOPE) {
error(
@@ -1003,8 +1029,9 @@ static UniqueTiles dedupTiles(
attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
attr.bank = tileID >= options.maxNbTiles[0];
attr.tileID =
(attr.bank ? tileID - options.maxNbTiles[0] : tileID) + options.baseTileIDs[attr.bank];
attr.tileID = (attr.bank ? tileID - options.maxNbTiles[0] : tileID)
+ options.baseTileIDs[attr.bank];
}
}
// Copy elision should prevent the contained `unordered_set` from being re-constructed
@@ -1014,7 +1041,9 @@ static UniqueTiles dedupTiles(
static void outputTileData(UniqueTiles const &tiles) {
File output;
if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) {
// LCOV_EXCL_START
fatal("Failed to create \"%s\": %s", output.c_str(options.output), strerror(errno));
// LCOV_EXCL_STOP
}
uint16_t tileID = 0;
@@ -1036,7 +1065,9 @@ static void outputTileData(UniqueTiles const &tiles) {
static void outputTilemap(DefaultInitVec<AttrmapEntry> const &attrmap) {
File output;
if (!output.open(options.tilemap, std::ios_base::out | std::ios_base::binary)) {
// LCOV_EXCL_START
fatal("Failed to create \"%s\": %s", output.c_str(options.tilemap), strerror(errno));
// LCOV_EXCL_STOP
}
for (AttrmapEntry const &entry : attrmap) {
@@ -1049,7 +1080,9 @@ static void outputAttrmap(
) {
File output;
if (!output.open(options.attrmap, std::ios_base::out | std::ios_base::binary)) {
// LCOV_EXCL_START
fatal("Failed to create \"%s\": %s", output.c_str(options.attrmap), strerror(errno));
// LCOV_EXCL_STOP
}
for (AttrmapEntry const &entry : attrmap) {
@@ -1065,7 +1098,9 @@ static void outputPalmap(
) {
File output;
if (!output.open(options.palmap, std::ios_base::out | std::ios_base::binary)) {
// LCOV_EXCL_START
fatal("Failed to create \"%s\": %s", output.c_str(options.palmap), strerror(errno));
// LCOV_EXCL_STOP
}
for (AttrmapEntry const &entry : attrmap) {
@@ -1093,6 +1128,7 @@ void process() {
// Now, we have all the image's colors in `colors`
// The next step is to order the palette
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
fputs("Image colors: [ ", stderr);
for (auto const &slot : colors) {
@@ -1103,6 +1139,19 @@ void process() {
}
fputs("]\n", stderr);
}
// LCOV_EXCL_STOP
if (options.palSpecType == Options::DMG) {
if (options.hasTransparentPixels) {
fatal(
"Image contains transparent pixels, not compatible with a DMG palette specification"
);
}
if (!png.isSuitableForGrayscale()) {
fatal("Image contains too many or non-gray colors, not compatible with a DMG palette "
"specification");
}
}
// Now, iterate through the tiles, generating proto-palettes as we go
// We do this unconditionally because this performs the image validation (which we want to
@@ -1118,7 +1167,8 @@ void process() {
std::unordered_set<uint16_t> tileColors;
for (uint32_t y = 0; y < 8; ++y) {
for (uint32_t x = 0; x < 8; ++x) {
if (Rgba color = tile.pixel(x, y); !color.isTransparent()) {
if (Rgba color = tile.pixel(x, y);
!color.isTransparent() || !options.hasTransparentPixels) {
tileColors.insert(color.cgbColor());
}
}
@@ -1136,6 +1186,7 @@ void process() {
if (tileColors.empty()) {
// "Empty" proto-palettes screw with the packing process, so discard those
assume(!isBgColorTransparent());
attrs.protoPaletteID = AttrmapEntry::transparent;
continue;
}
@@ -1145,6 +1196,21 @@ void process() {
protoPalette.add(cgbColor);
}
if (options.bgColor.has_value()
&& std::find(RANGE(tileColors), options.bgColor->cgbColor()) != tileColors.end()) {
if (tileColors.size() == 1) {
// The tile contains just the background color, skip it.
attrs.protoPaletteID = AttrmapEntry::background;
continue;
}
fatal(
"Tile (%" PRIu32 ", %" PRIu32 ") contains the background color (#%08x)!",
tile.x,
tile.y,
options.bgColor->toCSS()
);
}
// Insert the proto-palette, making sure to avoid overlaps
for (size_t n = 0; n < protoPalettes.size(); ++n) {
switch (protoPalette.compare(protoPalettes[n])) {
@@ -1152,17 +1218,6 @@ void process() {
protoPalettes[n] = protoPalette; // Override them
// Remove any other proto-palettes that we encompass
// (Example [(0, 1), (0, 2)], inserting (0, 1, 2))
//
// The following code does its job, except that references to the removed
// proto-palettes are not updated, causing issues.
// TODO: overlap might not be detrimental to the packing algorithm.
// Investigation is necessary, especially if pathological cases are found.
//
// for (size_t i = protoPalettes.size(); --i != n;) {
// if (protoPalette.compare(protoPalettes[i]) == ProtoPalette::WE_BIGGER) {
// protoPalettes.erase(protoPalettes.begin() + i);
// }
// }
[[fallthrough]];
case ProtoPalette::THEY_BIGGER:
@@ -1176,7 +1231,7 @@ void process() {
}
attrs.protoPaletteID = protoPalettes.size();
if (protoPalettes.size() == AttrmapEntry::transparent) { // Check for overflow
if (protoPalettes.size() == AttrmapEntry::background) { // Check for overflow
fatal(
"Reached %zu proto-palettes... sorry, this image is too much for me to handle :(",
AttrmapEntry::transparent
@@ -1192,6 +1247,7 @@ continue_visiting_tiles:;
protoPalettes.size(),
protoPalettes.size() != 1 ? "s" : ""
);
// LCOV_EXCL_START
if (options.verbosity >= Options::VERB_INTERM) {
for (auto const &protoPal : protoPalettes) {
fputs("[ ", stderr);
@@ -1201,11 +1257,13 @@ continue_visiting_tiles:;
fputs("]\n", stderr);
}
}
// LCOV_EXCL_STOP
if (options.palSpecType == Options::EMBEDDED) {
generatePalSpec(png);
}
auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC
auto [mappings, palettes] =
options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG
? generatePalettes(protoPalettes, png)
: makePalsAsSpecified(protoPalettes);
outputPalettes(palettes);

View File

@@ -195,12 +195,13 @@ void reverse() {
Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width, height
);
// TODO: `-U` to configure tile size beyond 8x8px ("deduplication units")
std::vector<std::array<std::optional<Rgba>, 4>> palettes{
{Rgba(0xFFFFFFFF), Rgba(0xAAAAAAFF), Rgba(0x555555FF), Rgba(0x000000FF)}
Rgba const grayColors[4] = {
Rgba(0xFFFFFFFF), Rgba(0xAAAAAAFF), Rgba(0x555555FF), Rgba(0x000000FF)
};
// If a palette file is used as input, it overrides the default colors.
std::vector<std::array<std::optional<Rgba>, 4>> palettes{
{grayColors[0], grayColors[1], grayColors[2], grayColors[3]}
};
// If a palette file or palette spec is used as input, it overrides the default colors.
if (!options.palettes.empty()) {
File file;
if (!file.open(options.palettes, std::ios::in | std::ios::binary)) {
@@ -257,6 +258,10 @@ void reverse() {
putc('\n', stderr);
}
}
} else if (options.palSpecType == Options::DMG) {
for (size_t i = 0; i < palettes[0].size(); ++i) {
palettes[0][i] = grayColors[options.dmgValue(i)];
}
} else if (options.palSpecType == Options::EMBEDDED) {
warning("An embedded palette was requested, but no palette file was specified; ignoring "
"request.");
@@ -401,7 +406,9 @@ void reverse() {
options.verbosePrint(Options::VERB_LOG_ACT, "Writing image...\n");
File pngFile;
if (!pngFile.open(options.input, std::ios::out | std::ios::binary)) {
// LCOV_EXCL_START
fatal("Failed to create \"%s\": %s", pngFile.c_str(options.input), strerror(errno));
// LCOV_EXCL_STOP
}
png_structp png = png_create_write_struct(
PNG_LIBPNG_VER_STRING,
@@ -410,46 +417,85 @@ void reverse() {
pngWarning
);
if (!png) {
// LCOV_EXCL_START
fatal("Failed to create PNG write struct: %s", strerror(errno));
// LCOV_EXCL_STOP
}
png_infop pngInfo = png_create_info_struct(png);
if (!pngInfo) {
fatal("Failed to create PNG info struct: %s", strerror(errno));
// LCOV_EXCL_START
fatal("Failed to create PNG info structure: %s", strerror(errno));
// LCOV_EXCL_STOP
}
png_set_write_fn(png, &pngFile, writePng, flushPng);
int pngColorType = options.palettes.empty() ? PNG_COLOR_TYPE_GRAY
: palettes.size() == 1 ? PNG_COLOR_TYPE_PALETTE
: PNG_COLOR_TYPE_RGB_ALPHA;
int pngDepth = options.palettes.empty() ? options.bitDepth : 8;
png_set_IHDR(
png,
pngInfo,
width * 8,
height * 8,
8,
PNG_COLOR_TYPE_RGB_ALPHA,
pngDepth,
pngColorType,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT
);
png_write_info(png, pngInfo);
if (pngColorType != PNG_COLOR_TYPE_GRAY) {
png_color_8 sbitChunk;
sbitChunk.red = 5;
sbitChunk.green = 5;
sbitChunk.blue = 5;
if (pngColorType == PNG_COLOR_TYPE_RGB_ALPHA) {
sbitChunk.alpha = 1;
}
png_set_sBIT(png, pngInfo, &sbitChunk);
}
constexpr uint8_t SIZEOF_TILE = 4 * 8; // 4 bytes/pixel (RGBA @ 8 bits/channel) * 8 pixels/tile
size_t const SIZEOF_ROW = width * SIZEOF_TILE;
std::vector<uint8_t> tileRow(8 * SIZEOF_ROW, 0xFF); // Data for 8 rows of pixels
if (pngColorType == PNG_COLOR_TYPE_PALETTE) {
assume(palettes.size() == 1);
png_color pngPalette[4] = {};
png_byte pngTrans[4] = {};
int nbPngColors = 0, nbPngTrans = 0;
for (auto const &color : palettes[0]) {
if (!color.has_value()) {
continue;
}
pngPalette[nbPngColors].red = color->red;
pngPalette[nbPngColors].green = color->green;
pngPalette[nbPngColors].blue = color->blue;
pngTrans[nbPngColors] = color->alpha;
++nbPngColors;
if (color->alpha < 255) {
nbPngTrans = nbPngColors;
}
}
png_set_PLTE(png, pngInfo, pngPalette, nbPngColors);
if (nbPngTrans > 0) {
png_set_tRNS(png, pngInfo, pngTrans, nbPngTrans, nullptr);
}
}
png_write_info(png, pngInfo);
// N bits/pixel * 8 pixels/tile row / 8 bits/byte = N bytes/tile row
uint8_t const bytesPerTileRow = pngColorType == PNG_COLOR_TYPE_RGB_ALPHA ? 32 : pngDepth;
size_t const bytesPerRow = width * bytesPerTileRow;
std::vector<uint8_t> tileRow(8 * bytesPerRow, 0xFF); // Data for 8 rows of pixels
uint8_t * const rowPtrs[8] = {
&tileRow.data()[0 * SIZEOF_ROW],
&tileRow.data()[1 * SIZEOF_ROW],
&tileRow.data()[2 * SIZEOF_ROW],
&tileRow.data()[3 * SIZEOF_ROW],
&tileRow.data()[4 * SIZEOF_ROW],
&tileRow.data()[5 * SIZEOF_ROW],
&tileRow.data()[6 * SIZEOF_ROW],
&tileRow.data()[7 * SIZEOF_ROW],
&tileRow.data()[0 * bytesPerRow],
&tileRow.data()[1 * bytesPerRow],
&tileRow.data()[2 * bytesPerRow],
&tileRow.data()[3 * bytesPerRow],
&tileRow.data()[4 * bytesPerRow],
&tileRow.data()[5 * bytesPerRow],
&tileRow.data()[6 * bytesPerRow],
&tileRow.data()[7 * bytesPerRow],
};
for (size_t ty = 0; ty < height; ++ty) {
@@ -482,19 +528,36 @@ void reverse() {
bitplane0 = flipTable[bitplane0];
bitplane1 = flipTable[bitplane1];
}
uint8_t *ptr = &rowPtrs[y][tx * SIZEOF_TILE];
uint8_t *ptr = &rowPtrs[y][tx * bytesPerTileRow];
uint16_t gray = 0;
for (uint8_t x = 0; x < 8; ++x) {
uint8_t bit0 = bitplane0 & 0x80, bit1 = bitplane1 & 0x80;
Rgba const &pixel = *palette[bit0 >> 7 | bit1 >> 6];
uint8_t colorID = bit0 >> 7 | bit1 >> 6;
Rgba const &pixel = *palette[colorID];
if (pngColorType == PNG_COLOR_TYPE_GRAY) {
gray = gray << pngDepth | (pixel.red & ((1 << pngDepth) - 1));
} else if (pngColorType == PNG_COLOR_TYPE_PALETTE) {
*ptr++ = palID * 4 + colorID;
} else {
*ptr++ = pixel.red;
*ptr++ = pixel.green;
*ptr++ = pixel.blue;
*ptr++ = pixel.alpha;
}
// Shift the pixel out
bitplane0 <<= 1;
bitplane1 <<= 1;
}
if (pngDepth == 1) {
*ptr = gray;
} else if (pngDepth == 2) {
*ptr++ = gray >> 8;
*ptr = gray & 0xff;
}
}
}
// We never modify the pointers, and neither should libpng, despite the overly lax function

View File

@@ -58,6 +58,15 @@ uint16_t Rgba::cgbColor() const {
uint8_t Rgba::grayIndex() const {
assume(isGray());
// Convert from [0; 256[ to [0; maxOpaqueColors[
return static_cast<uint16_t>(255 - red) * options.maxOpaqueColors() / 256;
// 2bpp shades are inverted from RGB PNG; %00 = white, %11 = black
uint8_t gray = 255 - red;
if (options.palSpecType == Options::DMG) {
assume(!options.hasTransparentPixels);
// Reduce gray shade from 0..<256 to 0..<4, then map to color index,
// then reduce to 0..<nbColorsPerPal
return options.dmgColors[gray * 4 / 256] * options.nbColorsPerPal / 4;
}
// Reduce gray shade from 0..<256 to hasTransparentPixels..<nbColorsPerPal
// Note that `maxOpaqueColors()` already takes `hasTransparentPixels` into account
return gray * options.maxOpaqueColors() / 256 + options.hasTransparentPixels;
}

View File

@@ -424,5 +424,5 @@ max_out:
}
}
unreachable_();
unreachable_(); // LCOV_EXCL_LINE
}

View File

@@ -163,6 +163,7 @@ static option const longopts[] = {
{nullptr, no_argument, nullptr, 0 }
};
// LCOV_EXCL_START
static void printUsage() {
fputs(
"Usage: rgblink [-dhMtVvwx] [-l script] [-m map_file] [-n sym_file]\n"
@@ -181,6 +182,7 @@ static void printUsage() {
stderr
);
}
// LCOV_EXCL_STOP
enum ScrambledRegion {
SCRAMBLE_ROMX,
@@ -335,8 +337,10 @@ int main(int argc, char *argv[]) {
isWRAM0Mode = true;
break;
case 'h':
// LCOV_EXCL_START
printUsage();
exit(0);
// LCOV_EXCL_STOP
case 'l':
if (linkerScriptName) {
warnx("Overriding linker script %s", linkerScriptName);
@@ -393,11 +397,15 @@ int main(int argc, char *argv[]) {
is32kMode = true;
break;
case 'V':
// LCOV_EXCL_START
printf("rgblink %s\n", get_package_version_string());
exit(0);
// LCOV_EXCL_STOP
case 'v':
// LCOV_EXCL_START
beVerbose = true;
break;
// LCOV_EXCL_STOP
case 'w':
isWRAM0Mode = true;
break;
@@ -407,8 +415,10 @@ int main(int argc, char *argv[]) {
is32kMode = true;
break;
default:
// LCOV_EXCL_START
printUsage();
exit(1);
// LCOV_EXCL_STOP
}
}

View File

@@ -15,6 +15,7 @@
#include "helpers.hpp"
#include "linkdefs.hpp"
#include "platform.hpp"
#include "util.hpp"
#include "link/main.hpp"
#include "link/section.hpp"
@@ -30,6 +31,7 @@ FILE *mapFile;
struct SortedSymbol {
Symbol const *sym;
uint16_t addr;
uint16_t parentAddr;
};
struct SortedSections {
@@ -259,24 +261,13 @@ static void writeROM() {
}
}
// Checks whether this character is legal as the first character of a symbol's name in a sym file
static bool canStartSymName(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
}
// Checks whether this character is legal in a symbol's name in a sym file
static bool isLegalForSymName(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'
|| c == '@' || c == '#' || c == '$' || c == '.';
}
// Prints a symbol's name to a file, assuming that the first character is legal.
// Illegal characters are UTF-8-decoded (errors are replaced by U+FFFD) and emitted as `\u`/`\U`.
// Illegal characters are UTF-8-decoded (errors are replaced by U+FFFD) and emitted as '\u'/'\U'.
static void printSymName(std::string const &name, FILE *file) {
for (char const *ptr = name.c_str(); *ptr != '\0';) {
char c = *ptr;
if (isLegalForSymName(c)) {
if (continuesIdentifier(c)) {
// Output legal ASCII characters as-is
putc(c, file);
++ptr;
@@ -306,33 +297,28 @@ static void printSymName(std::string const &name, FILE *file) {
}
// Comparator function for `std::stable_sort` to sort symbols
// Symbols are ordered by address, then by parentage
static bool compareSymbols(SortedSymbol const &sym1, SortedSymbol const &sym2) {
// First, sort by address
if (sym1.addr != sym2.addr) {
return sym1.addr < sym2.addr;
}
// Second, sort by locality (global before local)
std::string const &sym1_name = sym1.sym->name;
std::string const &sym2_name = sym2.sym->name;
bool sym1_local = sym1_name.find(".") != std::string::npos;
bool sym2_local = sym2_name.find(".") != std::string::npos;
bool sym1_local = sym1_name.find('.') != std::string::npos;
bool sym2_local = sym2_name.find('.') != std::string::npos;
if (sym1_local != sym2_local) {
size_t sym1_len = sym1_name.length();
size_t sym2_len = sym2_name.length();
// Sort parent labels before their child local labels
if (sym2_name.starts_with(sym1_name) && sym2_name[sym1_len] == '.') {
return true;
}
if (sym1_name.starts_with(sym2_name) && sym1_name[sym2_len] == '.') {
return false;
}
// Sort local labels before unrelated global labels
return sym1_local;
return sym1_local < sym2_local;
}
return false;
// Third, sort by parent address
if (sym1.parentAddr != sym2.parentAddr) {
return sym1.parentAddr < sym2.parentAddr;
}
// Fourth, sort by name
return sym1_name < sym2_name;
}
// Write a bank's contents to the sym file
@@ -368,11 +354,20 @@ static void writeSymBank(SortedSections const &bankSections, SectionType type, u
forEachSortedSection(sect, {
for (Symbol const *sym : sect->symbols) {
// Don't output symbols that begin with an illegal character
if (!sym->name.empty() && canStartSymName(sym->name[0])) {
symList.push_back({
.sym = sym,
.addr = static_cast<uint16_t>(sym->label().offset + sect->org),
});
if (!sym->name.empty() && startsIdentifier(sym->name[0])) {
uint16_t addr = static_cast<uint16_t>(sym->label().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 && parentSym->data.holds<Label>()) {
auto const &parentLabel = parentSym->label();
assume(parentLabel.section != nullptr);
parentAddr =
static_cast<uint16_t>(parentLabel.offset + parentLabel.section->org);
}
}
symList.push_back({.sym = sym, .addr = addr, .parentAddr = parentAddr});
}
}
});
@@ -471,11 +466,14 @@ static void writeMapBank(SortedSections const &sectList, SectionType type, uint3
// Also print symbols in the following "pieces"
for (uint16_t org = sect->org; sect; sect = sect->nextu.get()) {
for (Symbol *sym : sect->symbols) {
// Don't output symbols that begin with an illegal character
if (!sym->name.empty() && startsIdentifier(sym->name[0])) {
// Space matches "\tSECTION: $xxxx ..."
fprintf(mapFile, "\t $%04" PRIx32 " = ", sym->label().offset + org);
printSymName(sym->name, mapFile);
putc('\n', mapFile);
}
}
if (sect->nextu) {
// Announce the following "piece"

View File

@@ -363,7 +363,6 @@ static int32_t computeRPNExpr(Patch const &patch, std::vector<Symbol> const &fil
case RPN_RST:
value = popRPN(patch);
// Acceptable values are 0x00, 0x08, 0x10, ..., 0x38
// They can be easily checked with a bitmask
if (value & ~0x38) {
if (!isError) {
error(patch.src, patch.lineNo, "Value $%" PRIx32 " is not a RST vector", value);
@@ -374,6 +373,21 @@ static int32_t computeRPNExpr(Patch const &patch, std::vector<Symbol> const &fil
value |= 0xC7;
break;
case RPN_BIT_INDEX: {
value = popRPN(patch);
int32_t mask = getRPNByte(expression, size, patch);
// Acceptable values are 0 to 7
if (value & ~0x07) {
if (!isError) {
error(patch.src, patch.lineNo, "Value $%" PRIx32 " is not a bit index", value);
isError = true;
}
value = 0;
}
value = mask | (value << 3);
break;
}
case RPN_CONST:
value = 0;
for (uint8_t shift = 0; shift < 32; shift += 8) {

View File

@@ -228,7 +228,7 @@ static uint8_t parseHexDigit(int c) {
} else if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
} else {
unreachable_();
unreachable_(); // LCOV_EXCL_LINE
}
}
@@ -251,10 +251,16 @@ yy::parser::symbol_type yylex() {
if (c == EOF) {
// Basically yywrap().
if (lexerStack.size() != 1) {
if (!atEof) {
// Inject a newline at EOF to simplify parsing.
atEof = true;
return yy::parser::make_newline();
} else {
lexerStack.pop_back();
return yylex();
}
} else if (!atEof) {
// Inject a newline at EOF, to avoid errors for files that don't end with one.
// Inject a newline at EOF to simplify parsing.
atEof = true;
return yy::parser::make_newline();
} else {

View File

@@ -9,7 +9,7 @@
#include <string.h>
#include <tuple>
#include "helpers.hpp" // assume
#include "helpers.hpp" // assume, literal_strlen
#include "linkdefs.hpp"
#include "platform.hpp"
@@ -34,9 +34,10 @@ static char const *delim = " \f\n\r\t\v"; // Whitespace according to the C and P
static int
nextLine(std::vector<char> &lineBuf, uint32_t &lineNo, FileStackNode const &where, FILE *file) {
retry:
int firstChar;
for (;;) {
++lineNo;
int firstChar = getc(file);
firstChar = getc(file);
lineBuf.clear();
switch (firstChar) {
@@ -55,7 +56,9 @@ retry:
}
[[fallthrough]];
case '\n':
goto retry;
continue;
}
break;
}
for (;;) {
@@ -446,7 +449,7 @@ void sdobj_ReadFile(FileStackNode const &where, FILE *file, std::vector<Symbol>
// It's fine to keep modifying the symbol after `AddSymbol`, only
// the name must not be modified
}
if (strncasecmp(&token[1], "ef", 2) != 0) {
if (strncasecmp(&token[1], "ef", literal_strlen("ef")) != 0) {
fatal(&where, lineNo, "'S' line is neither \"Def\" nor \"Ref\"");
}

View File

@@ -208,8 +208,10 @@ static void mergeSections(Section &target, std::unique_ptr<Section> &&other) {
break;
case SECTION_NORMAL:
// LCOV_EXCL_START
unreachable_();
}
// LCOV_EXCL_STOP
// Note that the order in which fragments are stored in the `nextu` list does not
// really matter, only that offsets were properly computed above

View File

@@ -6,6 +6,15 @@
#include <stdint.h>
#include <stdio.h>
bool startsIdentifier(int c) {
// This returns false for anonymous labels, which internally start with a '!'
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '.' || c == '_';
}
bool continuesIdentifier(int c) {
return startsIdentifier(c) || (c >= '0' && c <= '9') || c == '#' || c == '$' || c == '@';
}
char const *printChar(int c) {
// "'A'" + '\0': 4 bytes
// "'\\n'" + '\0': 5 bytes

View File

@@ -5,7 +5,7 @@ error: anon-label-bad.asm(6):
error: anon-label-bad.asm(9):
syntax error, unexpected anonymous label
error: anon-label-bad.asm(10):
syntax error, unexpected anonymous label, expecting label or identifier or local identifier
syntax error, unexpected anonymous label, expecting symbol or label or local label
error: anon-label-bad.asm(22):
syntax error, unexpected ::
error: Assembly aborted (5 errors)!

View File

@@ -7,6 +7,17 @@ FOR V, 1, 100
PRINTLN "cont"
ENDR
WARN "done {d:V}"
rept 2
break
; skips nested code
rept 3
println "\tinner"
endr
if 1
println "\tconditional"
endc
println "outer"
endr
rept 1
break
; skips invalid code

View File

@@ -1,8 +1,8 @@
warning: break.asm(9): [-Wuser]
done 5
warning: break.asm(17): [-Wuser]
warning: break.asm(28): [-Wuser]
OK
error: break.asm(18):
error: break.asm(29):
BREAK can only be used inside a REPT/FOR block
FATAL: break.asm(19) -> break.asm::REPT~1(23):
FATAL: break.asm(30) -> break.asm::REPT~1(34):
Ended block with 1 unterminated IF construct

27
test/asm/charcmp.asm Normal file
View File

@@ -0,0 +1,27 @@
charmap "a", 1
charmap "b", 2
charmap "c", 0
charmap "w", 3, 2, 1
charmap "x", 1, 2
charmap "y", 2, 1
charmap "z", 1, 2, 3
macro test
println strfmt("\"%#s\" <=> \"%#s\" == %d", \1, \2, charcmp(\1, \2))
endm
test "", ""
test "a", "a"
test "aa", "aaa"
test "aaa", "aa"
test "a", "b"
test "b", "a"
test "", "b"
test "c", ""
test "abc", "cba"
test "cabc", "cxc"
test "zy", "abw"
test "abab", "xx"
test "abab", "ww"
test "w", "z"
test "xcy", "zw"

15
test/asm/charcmp.out Normal file
View File

@@ -0,0 +1,15 @@
"" <=> "" == 0
"a" <=> "a" == 0
"aa" <=> "aaa" == -1
"aaa" <=> "aa" == 1
"a" <=> "b" == -1
"b" <=> "a" == 1
"" <=> "b" == -1
"c" <=> "" == 1
"abc" <=> "cba" == 1
"cabc" <=> "cxc" == 0
"zy" <=> "abw" == 0
"abab" <=> "xx" == 0
"abab" <=> "ww" == -1
"w" <=> "z" == 1
"xcy" <=> "zw" == -1

View File

@@ -1,28 +0,0 @@
opt Wno-unmapped-char
charmap "<NULL>", $00
charmap "A", $10
charmap "B", $20
charmap "C", $30
charmap "Bold", $88
SECTION "test", ROM0
DEF S EQUS "XBold<NULL>ABC"
assert CHARLEN("{S}") == 6
println CHARSUB("{S}", 2)
assert !STRCMP(CHARSUB("{S}", 2), "Bold")
assert CHARSUB("{S}", -5) == CHARSUB("{S}", CHARLEN("{S}") + 1 - 5)
assert CHARSUB("{S}", 2) == "Bold" && "Bold" == $88
assert CHARSUB("{S}", 1) == $58 ; ASCII "X"
db "{S}"
newcharmap ascii
assert CHARLEN("{S}") == 14
println CHARSUB("{S}", 2)
assert !STRCMP(CHARSUB("{S}", 2), "B")
assert CHARSUB("{S}", -5) == CHARSUB("{S}", CHARLEN("{S}") + 1 - 5)
assert CHARSUB("{S}", 2) == "B" && "B" == $42 ; ASCII "B"
assert CHARSUB("{S}", 1) == $58 ; ASCII "X"
db "{S}"

View File

@@ -0,0 +1,34 @@
opt Wno-unmapped-char
charmap "<NULL>", $00
charmap "A", $10
charmap "B", $20
charmap "C", $30
charmap "Bold", $88
SECTION "test", ROM0
DEF S EQUS "XBold<NULL>ABC"
assert CHARLEN("{S}") == 6
println STRCHAR("{S}", 1)
assert !STRCMP(STRCHAR("{S}", 1), "Bold")
assert STRCHAR("{S}", -5) == STRCHAR("{S}", CHARLEN("{S}") - 5)
assert STRCHAR("{S}", 1) == "Bold" && "Bold" == $88
assert STRCHAR("{S}", 0) == $58 ; ASCII "X"
db "{S}"
for n, CHARLEN("{S}")
assert STRCHAR("{S}", n) == CHARSUB("{S}", n + 1)
assert STRCHAR("{S}", -n - 1) == CHARSUB("{S}", -n - 1)
endr
newcharmap ascii
assert CHARLEN("{S}") == 14
println STRCHAR("{S}", 1)
assert !STRCMP(STRCHAR("{S}", 1), "B")
assert STRCHAR("{S}", -5) == STRCHAR("{S}", CHARLEN("{S}") - 5)
assert STRCHAR("{S}", 1) == "B" && "B" == $42 ; ASCII "B"
assert STRCHAR("{S}", 0) == $58 ; ASCII "X"
db "{S}"

20
test/asm/charsize.asm Normal file
View File

@@ -0,0 +1,20 @@
charmap "a", 1
charmap "b", 2, 3
charmap "cdef", 4
charmap "ghi", 5, 6, 7, 8, 9
charmap "jkl", 123, 456, 789
charmap "mno", 123456789
charmap "¡Pokémon!", 2, 3
assert charsize("a") == 1
assert charsize("b") == 2
assert charsize("cdef") == 1
assert charsize("ghi") == 5
assert charsize("jkl") == 3
assert charsize("mno") == 1
assert charsize("¡Pokémon!") == 2
assert charsize("") == 0
assert charsize("hello world") == 0
assert charsize("abcdef") == 0
assert charsize("é") == 0

9
test/asm/charsize.err Normal file
View File

@@ -0,0 +1,9 @@
error: charsize.asm(17):
CHARSIZE: No character mapping for ""
error: charsize.asm(18):
CHARSIZE: No character mapping for "hello world"
error: charsize.asm(19):
CHARSIZE: No character mapping for "abcdef"
error: charsize.asm(20):
CHARSIZE: No character mapping for "é"
error: Assembly aborted (4 errors)!

23
test/asm/charval.asm Normal file
View File

@@ -0,0 +1,23 @@
charmap "a", 1
charmap "b", 2, 3
charmap "cdef", 4
charmap "ghi", 5, 6, 7, 8, 9
charmap "jkl", 123, 456, 789
charmap "mno", 123456789
assert charval("a", 0) == 1
assert charval("a", -1) == 1
assert charval("b", 0) == 2
assert charval("b", 1) == 3
assert charval("b", -1) == 3
assert charval("b", -2) == 2
assert charval("cdef", 0) == 4
assert charval("ghi", 2) == charval("ghi", -3)
assert charval("jkl", -1) == 789
assert charval("mno", 0) == 123456789
assert charval("abc", 0) == 0
assert charval("cd", 1) == 0
assert charval("xyz", 2) == 0
assert charval("ghi", -10) == 5
assert charval("ghi", 10) == 0

11
test/asm/charval.err Normal file
View File

@@ -0,0 +1,11 @@
error: charval.asm(19):
CHARVAL: No character mapping for "abc"
error: charval.asm(20):
CHARVAL: No character mapping for "cd"
error: charval.asm(21):
CHARVAL: No character mapping for "xyz"
warning: charval.asm(22): [-Wbuiltin-args]
CHARVAL: Index starts at 0
warning: charval.asm(23): [-Wbuiltin-args]
CHARVAL: Index 10 is past the end of the character mapping
error: Assembly aborted (3 errors)!

View File

@@ -1,3 +1,4 @@
error: command-line-symbols.asm(3):
'FOO' already defined at <command-line>
(should it be {interpolated} to define its contents "hello"?)
error: Assembly aborted (1 error)!

View File

@@ -15,3 +15,6 @@ SECTION "Unaligned", ROM0
println @ & 0
println @ & 1 ; Nope
Floating:
println Floating & Aligned ; Neither defined

View File

@@ -4,4 +4,6 @@ error: const-and.asm(11):
Expected constant expression: 'Aligned' is not constant at assembly time
error: const-and.asm(17):
Expected constant expression: PC is not constant at assembly time
error: Assembly aborted (3 errors)!
error: const-and.asm(20):
Expected constant expression: 'Floating' is not constant at assembly time
error: Assembly aborted (4 errors)!

View File

@@ -5,3 +5,4 @@ $0
$A
$0
$0
$0

View File

@@ -1,7 +1,7 @@
error: def-scoped.asm(10):
syntax error, unexpected local identifier, expecting identifier
syntax error, unexpected local label, expecting symbol
error: def-scoped.asm(13):
syntax error, unexpected local identifier, expecting identifier
syntax error, unexpected local label, expecting symbol
error: def-scoped.asm(16):
syntax error, unexpected local identifier, expecting identifier
syntax error, unexpected local label, expecting symbol
error: Assembly aborted (3 errors)!

View File

@@ -28,3 +28,15 @@ redef string equs "there"
redef constant equ 6*9
println constant
macro mac
endm
redef mac equ 42
def name equs "constant2"
def name equ 1337
redef name equs "mac2"
macro name
endm

View File

@@ -1,3 +1,11 @@
error: def.asm(23):
'constant' already defined at def.asm(10)
error: Assembly aborted (1 error)!
error: def.asm(35):
'mac' already defined as non-EQU at def.asm(32)
error: def.asm(38):
'name' already defined at def.asm(37)
(should it be {interpolated} to define its contents "constant2"?)
error: def.asm(42):
'name' already defined at def.asm(40)
(should it be {interpolated} to define its contents "mac2"?)
error: Assembly aborted (4 errors)!

View File

@@ -1,5 +1,5 @@
error: ds-bad.asm(3):
Expected constant expression: 'unknown' is not constant at assembly time
warning: ds-bad.asm(4): [-Wtruncation]
Expression must be 8-bit
Expression must be 8-bit; use LOW() to force 8-bit
error: Assembly aborted (1 error)!

View File

@@ -3,6 +3,5 @@ warning: equs-newline.asm(3): [-Wuser]
while expanding symbol "ACT"
warning: equs-newline.asm(3): [-Wuser]
Second
while expanding symbol "ACT"
warning: equs-newline.asm(4): [-Wuser]
Third

View File

@@ -2,7 +2,7 @@
println 42, 1 2 3 4
for n, 5
for n, 3
println "start {d:n}"
println syntax error
println "finish {d:n}"

View File

@@ -1,5 +1,9 @@
error: error-recovery.asm(3):
syntax error, unexpected number
error: error-recovery.asm(5) -> error-recovery.asm::REPT~1(7):
syntax error, unexpected identifier
error: Assembly aborted (2 errors)!
syntax error, unexpected symbol
error: error-recovery.asm(5) -> error-recovery.asm::REPT~2(7):
syntax error, unexpected symbol
error: error-recovery.asm(5) -> error-recovery.asm::REPT~3(7):
syntax error, unexpected symbol
error: Assembly aborted (4 errors)!

View File

@@ -1,4 +1,8 @@
begin
$2Astart 0
finish 0
end 0
start 1
finish 1
start 2
finish 2
end 3

View File

@@ -0,0 +1,13 @@
def x = 4
for x, x
def x *= 2
println x
endr
println x
def y equ 5
for y, y
def y *= 3
println y
endr
println y

View File

@@ -0,0 +1,3 @@
error: for-loop-variable.asm(12):
'y' already defined as constant at for-loop-variable.asm(8)
error: Assembly aborted (1 error)!

View File

@@ -0,0 +1,6 @@
$0
$2
$4
$6
$4
$5

View File

@@ -6,6 +6,7 @@ warning: for.asm(20): [-Wbackwards-for]
FOR goes backwards from 1 to 2 by -1
error: for.asm(46):
's' already defined as constant at for.asm(39)
(should it be {interpolated} to define its contents "x"?)
error: for.asm(48) -> for.asm::REPT~4(54):
'v' already defined as constant at for.asm(48) -> for.asm::REPT~4(52)
FATAL: for.asm(48) -> for.asm::REPT~4(54):

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