Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions nanoprintf.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,15 @@ NPF_VISIBILITY int npf_vpprintf(
!defined(NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS) && \
!defined(NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS) && \
!defined(NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS) && \
!defined(NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS)
!defined(NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS) && \
!defined(NANOPRINTF_USE_ALT_FORM_MODIFIER)
#define NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS 0
#define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 0
#define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 0
#define NANOPRINTF_USE_ALT_FORM_MODIFIER 1
#endif

// If anything's been configured, everything must be configured.
Expand Down Expand Up @@ -246,19 +248,23 @@ enum {
typedef struct npf_format_spec {
#if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1
int field_width;
uint8_t field_width_opt;
char left_justified; // '-'
char leading_zero_pad; // '0'
#endif
#if NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS == 1
int prec;
uint8_t prec_opt;
#endif
char prepend; // ' ' or '+'
#if NANOPRINTF_USE_ALT_FORM_MODIFIER == 1
char alt_form; // '#'
#endif
char case_adjust; // 'a' - 'A'
uint8_t length_modifier;
uint8_t conv_spec;
#if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1
uint8_t field_width_opt;
char left_justified; // '-'
char leading_zero_pad; // '0'
#endif
} npf_format_spec_t;

#if NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS == 0
Expand Down Expand Up @@ -295,7 +301,9 @@ static int npf_parse_format_spec(char const *format, npf_format_spec_t *out_spec
#endif
out_spec->case_adjust = 'a' - 'A'; // lowercase
out_spec->prepend = 0;
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
Copy link
Owner

Choose a reason for hiding this comment

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

Please change these tests to #if NANOPRINTF_USE_ALT_FORM_MODIFIER == 1 (For better or worse, the rest of the file uses the == 1 convention so I'd prefer to preserve it, and if it does change, do a sweeping change all at once).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wrote "Removing it --ie still parsing it, but ignoring it--", whereas the actual code I committed skips the parsing too.
I'm not sure if you prefer the "parse but ignore" or the "fail parsing" route.
As I've commented elsewhere, we could also consider a different option for "everything that is disabled will not be honored, but will still be parsed".

I've corrected the code as you asked.

Also: if you find it easier to just take the code, tweak it, and commit it in your own branch, without having to accept the PR --which might require slower back and forth discussions with me--, that's perfectly fine. No need for changes to the contributors list or merging from my fork.

Finally, I have problems with running all the tests as specified in the readme, so it would be great if you could help me out here.
Running the build.py script, I get an HTTP error. I can give more details if you can help.
I don't understand how to build the tests myself, so right now I'm just working with a reduced set of tests in a separate file of my own.
Even if I don't use the python build, what am I supposed to compile all the "unit*.cc", "conformance.cc", "doctest_main.cc", and "paland.cc" ? All of them together don't compile. I managed to compile some of them, but the tests fail because of some conformance checks that compare implementation-defined behavior against my local standard library, which is not guaranteed to behave like NPF.

Copy link
Owner

Choose a reason for hiding this comment

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

Sure, I'm happy to take this as a proof of concept and do it via a local PR at my bench if that's easier for you. I can do all the test work as well if you'd like.

Nanoprintf uses a python script to kick off CMake to generate a ninja build that compiles the 2^N flag configuration explosion. It's more than I'd do for a less-configurable project, but if nanoprintf offers a flag then I need to test that every possible build combination works with or without that flag. So, the CMakeLists.txt file has a large and very nested loop for each flag, and emits a configuration target with that particular combination of flags.

You should just be able to clone npf and run ./b --paland -v - the python script looks for cmake and ninja in your system and, if they're absent, downloads them to the local transient "build" directory in your repo directory. (It doesn't attempt to install them or anything rude). Then, having either found or downloaded them, it simply runs cmake to configure and then ninja to build.

If that's not working for you, it might be a bug or platform incompatibility in the python script- I only test that it works on linux, so if you're on a different platform then it may not even work. (Again, the philosophy of "if it's not harnessed under CI, it's probably broken" shows itself!)

If you have the time and gumption, could I ask you to open an issue with output so that I can take a look? Generally most clients simply use nanoprintf as a submodule or just grab the .h file -- that's what it's there for -- so the build and test machinery has been lower priority. I'd still like it to work, though!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's fine if you work on your local PR.
I'll still try to run the tests, as that can speed things up.

I'm having trouble with the non-existent:
https://cmake.org/files/v3.22/cmake-3.22.2-win64-x86_64.tar.gz
which makes the build script fail.
I'll try and download cmake manually when I have time, and go on from there.
Do you think cmake 3.31 vs 4.0 should make any difference?

I'm on Windows, by the way. So I ignore "b" and run python build.py

Copy link
Owner

Choose a reason for hiding this comment

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

I just landed a PR that tests windows downloading and fixed a few issues- cmake changed its download file naming scheme and extension for windows, which explains and fixes the error you saw.

So, python build.py --download --paland -v will force the downloading and sandboxing of the latest ninja + cmake versions, and give you as close of a local experience as possible to what the build server is doing.

If you're curious, you can go here: https://github.com/charlesnicholson/nanoprintf/actions/runs/14815979224/job/41596450464?pr=300
and expand the "Build" tab to see what it's doing. Hopefully it will be the same on your computer? Please let me know if you try it out.

I'll start the PR integration work tomorrow, by the way, thanks again for all the PRs and very thoughtful discussion!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm running python build.py from a cmd prompt. I have MinGW "installed" (placed somewhere reachable from the system PATH).
I just tried building from git bash, and the error is the same -- it is still finding MinGW and compiling with that.
I don't know how to run it "inside of a visual studio developer command prompt". Do you mean running python build.py in the console of a VS code installation, or of full Visual Studio? I'm not familiar with that, but I could try. Or I could download clang and get the build script to find that instead of MinGW's GCC.

Copy link
Owner

Choose a reason for hiding this comment

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

Fascinating- sorry you're going through this; you're challenging a bunch of my assumptions about windows development :) I was in videogames + windows app development for a decade or so, and having a full Visual Studio installation was something that everybody always had for any software projects, so I'm clearly taking that for granted.

When you have Visual Studio installed for C/C++ development, it installs a shortcut that will launch powershell or the command prompt with various environment variables set up. Those update your PATH to include directories that contain programs like "cl.exe" and "link.exe" which are used for command-line compilation via MSVC. When those path entries are present, CMake will discover those tools during its host-platform-interrogation phase, and say "oh, I see, I'm doing a visual studio build." You can see CI run the script that sets the environment up for 32- or 64-bit compilation mode here: https://github.com/charlesnicholson/nanoprintf/blob/main/.github/workflows/presubmit.yml#L196

There's no particular reason that nanoprintf shouldn't work with mingw-based setups; I just haven't done the work to set it up because it's not how I develop when I'm on windows!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had a VS Code lying around from 2020, with the ms-vscode.cpptools extension installed.
I tried building from its terminal but it still finds MinGW. I hoped it would come with the necessary tools you described, but apparently that's only with full VS.
I'll see if I can get it, though it was already really heavyweight the last time I installed it years ago.

Copy link
Owner

Choose a reason for hiding this comment

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

I've never tried it through VS Code; I always do the (yes, really heavyweight) full VS installation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

VS's 26GB are really too much for my home connection.
Instead, I downloaded clang from here: https://github.com/llvm/llvm-project/releases
And I got CMake to find it instead, despite weird problems with Windows cmd.
However, now I get the error

[1/811] C:\Program_Files\clang\bin\clang++.exe -DDOCTEST_CONFIG_SUPER_FAST_ASSERTS -DNANOPRINTF_USE_ALT_FORM_FLAG=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0  -Os -std=gnu++17 -D_DLL -D_MT -Xclang --dependent-lib=msvcrt -pedantic -Wall -Wextra -Wundef -Werror -Weverything -MD -MT CMakeFiles/npf_paland.dir/tests/mpaland-conformance/paland.cc.obj -MF CMakeFiles\npf_paland.dir\tests\mpaland-conformance\paland.cc.obj.d -o CMakeFiles/npf_paland.dir/tests/mpaland-conformance/paland.cc.obj -c C:/npf/tests/mpaland-conformance/paland.cc
FAILED: CMakeFiles/npf_paland.dir/tests/mpaland-conformance/paland.cc.obj 
C:\Program_Files\clang\bin\clang++.exe -DDOCTEST_CONFIG_SUPER_FAST_ASSERTS -DNANOPRINTF_USE_ALT_FORM_FLAG=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0  -Os -std=gnu++17 -D_DLL -D_MT -Xclang --dependent-lib=msvcrt -pedantic -Wall -Wextra -Wundef -Werror -Weverything -MD -MT CMakeFiles/npf_paland.dir/tests/mpaland-conformance/paland.cc.obj -MF CMakeFiles\npf_paland.dir\tests\mpaland-conformance\paland.cc.obj.d -o CMakeFiles/npf_paland.dir/tests/mpaland-conformance/paland.cc.obj -c C:/npf/tests/mpaland-conformance/paland.cc
In file included from C:/npf/tests/mpaland-conformance/paland.cc:62:
C:/npf/tests/mpaland-conformance\../doctest.h:978:56: error: no member named 'operator<<' in the global namespace
  978 |     struct has_global_insertion_operator<T, decltype(::operator<<(declval<std::ostream&>(), declval<const T&>()), void())> : types::true_type { };

      |                                                      ~~^

I find it strange that this clang invocation gets the "-std=gnu++17" option rather than "-std=c++17". I can't find such an option in CMakeLists.txt

I tried adding add_compile_options("-std=c++17") somewhere in CMakeLists.txt, and it is indeed used now, but the error remains the same.

Also strange: doctest.h:978 is a line of code guarded by #if defined(_MSC_VER) && _MSC_VER <= 1900, so why is it being compiled by clang?

Building from cmd and from git bash is identical: it uses clang, and produces this error.

There is also a --dependent-lib=msvcrt in the compilation command. Could it be a cause/symptom of the problem?

out_spec->alt_form = 0;
#endif

while (*++cur) { // cur points at the leading '%' character
switch (*cur) { // Optional flags
Expand All @@ -305,7 +313,9 @@ static int npf_parse_format_spec(char const *format, npf_format_spec_t *out_spec
#endif
case '+': out_spec->prepend = '+'; continue;
case ' ': if (out_spec->prepend == 0) { out_spec->prepend = ' '; } continue;
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
case '#': out_spec->alt_form = '#'; continue;
#endif
default: break;
}
break;
Expand Down Expand Up @@ -556,7 +566,11 @@ static int npf_ftoa_rev(char *buf, npf_format_spec_t const *spec, double f) {

uint_fast8_t carry; carry = 0;
npf_ftoa_dec_t end, dec; dec = (npf_ftoa_dec_t)spec->prec;
if (dec || spec->alt_form) {
if (dec
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
|| spec->alt_form
#endif
) {
buf[dec++] = '.';
}

Expand Down Expand Up @@ -879,9 +893,11 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) {
#endif
if (!val && (fs.prec_opt != NPF_FMT_SPEC_OPT_NONE) && !fs.prec) {
// Zero value and explicitly-requested zero precision means "print nothing".
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
if ((fs.conv_spec == NPF_FMT_SPEC_CONV_OCTAL) && fs.alt_form) {
fs.prec = 1; // octal special case, print a single '0'
}
#endif
} else
#endif
#if NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS == 1
Expand All @@ -895,17 +911,21 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) {
cbuf_len = npf_utoa_rev(val, cbuf, base, fs.case_adjust);
}

#if NANOPRINTF_USE_ALT_FORM_MODIFIER
if (val && fs.alt_form && (fs.conv_spec == NPF_FMT_SPEC_CONV_OCTAL)) {
cbuf[cbuf_len++] = '0'; // OK to add leading octal '0' immediately.
}
#endif

#if NANOPRINTF_USE_ALT_FORM_MODIFIER
if (val && fs.alt_form) { // 0x or 0b but can't write it yet.
if (fs.conv_spec == NPF_FMT_SPEC_CONV_HEX_INT) { need_0x = 'X'; }
#if NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS == 1
else if (fs.conv_spec == NPF_FMT_SPEC_CONV_BINARY) { need_0x = 'B'; }
#endif
if (need_0x) { need_0x += fs.case_adjust; }
}
#endif
} break;

case NPF_FMT_SPEC_CONV_POINTER: {
Expand Down
16 changes: 16 additions & 0 deletions tests/conformance.cc
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ TEST_CASE("conformance to system printf") {
#endif // NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS
require_conform("%", "% %");
require_conform("%", "%+%");
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
require_conform("%", "%#%");
#endif
// require_conform(" %", "%10%"); clang adds width, gcc doesn't
// require_conform("% ", "%-10%"); clang adds -width, gcc doesn't
// require_conform(" %", "%10.10%"); clang adds width + precision.
Expand Down Expand Up @@ -240,7 +242,9 @@ TEST_CASE("conformance to system printf") {

SUBCASE("octal") {
require_conform("0", "%o", 0);
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
require_conform("0", "%#o", 0);
#endif
require_conform("37777777777", "%o", UINT_MAX);
require_conform("17", "%ho", (1 << 29u) + 15u);
#if ULONG_MAX > UINT_MAX
Expand All @@ -250,7 +254,9 @@ TEST_CASE("conformance to system printf") {

#if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1
require_conform(" 2322", "%10o", 1234);
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
require_conform(" 02322", "%#10o", 1234);
#endif
require_conform("0001", "%04o", 1);
require_conform("0000", "%04o", 0);
require_conform("0", "%+o", 0);
Expand All @@ -261,7 +267,9 @@ TEST_CASE("conformance to system printf") {

#if NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS == 1
require_conform("", "%.0o", 0);
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
require_conform("0", "%#.0o", 0);
#endif
#endif // NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS

#if (NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS == 1)
Expand Down Expand Up @@ -294,7 +302,9 @@ TEST_CASE("conformance to system printf") {
require_conform("0", "%X", 0);
require_conform("90ABCDEF", "%X", 0x90ABCDEF);
require_conform("FFFFFFFF", "%X", UINT_MAX);
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
require_conform("0", "%#x", 0);
#endif
require_conform("0", "%+x", 0);
require_conform("1", "%+x", 1);
require_conform("7b", "%hx", (1 << 26u) + 123u);
Expand All @@ -305,7 +315,9 @@ TEST_CASE("conformance to system printf") {

#if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1
require_conform(" 1234", "%10x", 0x1234);
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
require_conform(" 0x1234", "%#10x", 0x1234);
#endif
require_conform("0001", "%04u", 1);
require_conform("0000", "%04u", 0);
require_conform(" 0", "% 6x", 0);
Expand All @@ -315,7 +327,9 @@ TEST_CASE("conformance to system printf") {
#if NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS == 1
require_conform("", "%.0x", 0);
require_conform("", "%.0X", 0);
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
require_conform("", "%#.0X", 0);
#endif
#endif // NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS

#if (NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS == 1)
Expand Down Expand Up @@ -480,7 +494,9 @@ TEST_CASE("conformance to system printf") {
require_conform("0.00", "%.2f", 0.0);
require_conform("1.0", "%.1f", 1.0);
require_conform("1", "%.0f", 1.0);
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
require_conform("1.", "%#.0f", 1.0);
#endif
require_conform("1.00000000000", "%.11f", 1.0);
require_conform("1.5", "%.1f", 1.5);
require_conform("+1.5", "%+.1f", 1.5);
Expand Down
2 changes: 2 additions & 0 deletions tests/unit_binary.cc
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ TEST_CASE("binary") {
}
#endif

#if NANOPRINTF_USE_ALT_FORM_MODIFIER
SUBCASE("alternate form") {
require_equal("0", "%#b", 0);
require_equal("0b1", "%#b", 1);
Expand All @@ -146,4 +147,5 @@ TEST_CASE("binary") {
#endif
#endif
}
#endif
}
2 changes: 2 additions & 0 deletions tests/unit_ftoa_rev.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ TEST_CASE("ftoa_rev") {
SUBCASE("zero and decimal separator") {
require_ftoa_rev("0", +0.);
require_ftoa_rev("0", -0.);
#if NANOPRINTF_USE_ALT_FORM_MODIFIER
spec.alt_form = '#';
require_ftoa_rev("0.", 0.);
#endif
spec.prec = 1;
require_ftoa_rev("0.0", 0.);
}
Expand Down
2 changes: 2 additions & 0 deletions tests/unit_parse_format_spec.cc
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ TEST_CASE("npf_parse_format_spec") {
conversions, the behavior is undefined.
*/

#if NANOPRINTF_USE_ALT_FORM_MODIFIER
REQUIRE(!npf_parse_format_spec("%#", &spec)); // alternative form alone

SUBCASE("alternative form off by default") {
Expand All @@ -120,6 +121,7 @@ TEST_CASE("npf_parse_format_spec") {
REQUIRE(npf_parse_format_spec("%#####u", &spec) == 7);
REQUIRE(spec.alt_form);
}
#endif

/*
'0': For d, i, o, u, x, X, a, A, e, E, f, F, g, and G conversions, leading
Expand Down
2 changes: 2 additions & 0 deletions tests/unit_vpprintf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ TEST_CASE("npf_vpprintf") {
REQUIRE(r.String() == std::string{"ABCD"});
}

#if NANOPRINTF_USE_ALT_FORM_MODIFIER
SUBCASE("alternative flag: hex doesn't prepend 0x if value is 0") {
REQUIRE(npf_pprintf(r.PutC, &r, "%#x", 0) == 1);
REQUIRE(r.String() == std::string{"0"});
Expand All @@ -285,4 +286,5 @@ TEST_CASE("npf_vpprintf") {
REQUIRE(npf_pprintf(r.PutC, &r, "%#o", 2) == 2);
REQUIRE(r.String() == std::string{"02"});
}
#endif
}
Loading