From da62f2ba42d1bd572d99cb419fc556628eb0cc07 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sat, 19 Aug 2023 11:02:30 +0200 Subject: [PATCH 001/430] Get output_requirements struct its own include file --- src/middle-ram.cpp | 1 + src/options.hpp | 25 ------------------------ src/output-requirements.hpp | 38 +++++++++++++++++++++++++++++++++++++ src/output.cpp | 4 +++- src/output.hpp | 3 ++- tests/test-middle.cpp | 1 + 6 files changed, 45 insertions(+), 27 deletions(-) create mode 100644 src/output-requirements.hpp diff --git a/src/middle-ram.cpp b/src/middle-ram.cpp index f34161101..a00339970 100644 --- a/src/middle-ram.cpp +++ b/src/middle-ram.cpp @@ -10,6 +10,7 @@ #include "logging.hpp" #include "middle-ram.hpp" #include "options.hpp" +#include "output-requirements.hpp" #include #include diff --git a/src/options.hpp b/src/options.hpp index 1e18c31c3..dea7367a0 100644 --- a/src/options.hpp +++ b/src/options.hpp @@ -49,31 +49,6 @@ struct database_options_t std::string build_conninfo(database_options_t const &opt); -/** - * Outputs can signal their requirements to the middle by setting these fields. - */ -struct output_requirements -{ - /** - * Need full node objects with tags, attributes (only if --extra-attributes - * is set) and locations. If false, only node locations are needed. - */ - bool full_nodes = false; - - /** - * Need full way objects with tags, attributes (only if --extra-attributes - * is set) and way nodes. If false, only way nodes are needed. - */ - bool full_ways = false; - - /** - * Need full relation objects with tags, attributes (only if - * --extra-attributes is set) and members. If false, no data from relations - * is needed. - */ - bool full_relations = false; -}; - /** * Structure for storing command-line and other options */ diff --git a/src/output-requirements.hpp b/src/output-requirements.hpp new file mode 100644 index 000000000..9deb1a099 --- /dev/null +++ b/src/output-requirements.hpp @@ -0,0 +1,38 @@ +#ifndef OSM2PGSQL_OUTPUT_REQUIREMENTS_HPP +#define OSM2PGSQL_OUTPUT_REQUIREMENTS_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +/** + * Outputs can signal their requirements to the middle by setting these fields. + */ +struct output_requirements +{ + /** + * Need full node objects with tags, attributes (only if --extra-attributes + * is set) and locations. If false, only node locations are needed. + */ + bool full_nodes = false; + + /** + * Need full way objects with tags, attributes (only if --extra-attributes + * is set) and way nodes. If false, only way nodes are needed. + */ + bool full_ways = false; + + /** + * Need full relation objects with tags, attributes (only if + * --extra-attributes is set) and members. If false, no data from relations + * is needed. + */ + bool full_relations = false; +}; + +#endif // OSM2PGSQL_OUTPUT_REQUIREMENTS_HPP diff --git a/src/output.cpp b/src/output.cpp index 4c1d5787c..a26206f7d 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -7,12 +7,14 @@ * For a full list of authors see the git log. */ +#include "output.hpp" + #include "db-copy.hpp" #include "format.hpp" +#include "options.hpp" #include "output-gazetteer.hpp" #include "output-null.hpp" #include "output-pgsql.hpp" -#include "output.hpp" #ifdef HAVE_LUA # include "output-flex.hpp" diff --git a/src/output.hpp b/src/output.hpp index 35a787f61..c82a62d36 100644 --- a/src/output.hpp +++ b/src/output.hpp @@ -18,13 +18,14 @@ #include -#include "options.hpp" #include "osmtypes.hpp" +#include "output-requirements.hpp" class db_copy_thread_t; class thread_pool_t; struct middle_query_t; +struct options_t; class output_t { diff --git a/tests/test-middle.cpp b/tests/test-middle.cpp index 75c5dff84..f6ef975eb 100644 --- a/tests/test-middle.cpp +++ b/tests/test-middle.cpp @@ -18,6 +18,7 @@ #include "dependency-manager.hpp" #include "middle-pgsql.hpp" #include "middle-ram.hpp" +#include "output-requirements.hpp" #include "common-buffer.hpp" #include "common-cleanup.hpp" From fbc3ff9063177622af09ce126b508cc6e6c2cc6b Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Thu, 17 Aug 2023 09:13:37 +0200 Subject: [PATCH 002/430] Suppress clang-tidy error on main functions The main functions now catch all exceptions, but there is still the possibility that the logging of the error message throws again. Not much we can do about this. --- src/gen/osm2pgsql-gen.cpp | 1 + src/osm2pgsql.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index 75f886b74..64369585c 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -595,6 +595,7 @@ void genproc_t::run() } } +// NOLINTNEXTLINE(bugprone-exception-escape) int main(int argc, char *argv[]) { try { diff --git a/src/osm2pgsql.cpp b/src/osm2pgsql.cpp index 59333dfb8..656954454 100644 --- a/src/osm2pgsql.cpp +++ b/src/osm2pgsql.cpp @@ -348,6 +348,7 @@ static void check_and_set_style(options_t *options) } } +// NOLINTNEXTLINE(bugprone-exception-escape) int main(int argc, char *argv[]) { try { @@ -404,6 +405,9 @@ int main(int argc, char *argv[]) } catch (std::exception const &e) { log_error("{}", e.what()); return 1; + } catch (...) { + log_error("Unknown exception."); + return 1; } return 0; From 1f04c537f919a972e94c89c3004faefb723d613a Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Thu, 17 Aug 2023 09:26:41 +0200 Subject: [PATCH 003/430] Avoid narrowing conversion --- src/tagtransform-lua.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tagtransform-lua.cpp b/src/tagtransform-lua.cpp index fd2d035e4..3389dc165 100644 --- a/src/tagtransform-lua.cpp +++ b/src/tagtransform-lua.cpp @@ -110,8 +110,8 @@ bool lua_tagtransform_t::filter_tags(osmium::OSMObject const &o, bool *polygon, lua_pushstring(lua_state(), t.key.c_str()); lua_pushstring(lua_state(), t.value.c_str()); lua_rawset(lua_state(), -3); + ++sz; } - sz += tags.size(); } lua_pushinteger(lua_state(), sz); From 1c2a4251bceefecfc025a229757a0e76ac5571ec Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Thu, 17 Aug 2023 09:23:20 +0200 Subject: [PATCH 004/430] Refactor tests triggering a moved-from-warning Clang-tidy doesn't understand that only one SECTION is run per test so it thinks data is used after move. This removes some of the SECTIONs which don't really do things relevant to the tests here. And one test is split into two so we don't need the SECTION any more. --- tests/test-expire-from-geometry.cpp | 41 +++++++++++------------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/tests/test-expire-from-geometry.cpp b/tests/test-expire-from-geometry.cpp index fdd5a9fb0..87fa917b4 100644 --- a/tests/test-expire-from-geometry.cpp +++ b/tests/test-expire-from-geometry.cpp @@ -390,13 +390,6 @@ TEST_CASE("expire multilinestring geometry", "[NoDB]") et.from_geometry(geom, expire_config); } - SECTION("geom with check") - { - geom::geometry_t geom{std::move(ml)}; - geom.set_srid(3857); - et.from_geometry_if_3857(geom, expire_config); - } - auto const tiles = et.get_tiles(); REQUIRE(tiles.size() == 3); CHECK(tile_t::from_quadkey(tiles[0], zoom) == tile_t{zoom, 2049, 2046}); @@ -440,13 +433,6 @@ TEST_CASE("expire multipolygon geometry", "[NoDB]") et.from_geometry(geom, expire_config); } - SECTION("geom with check") - { - geom::geometry_t geom{std::move(mp)}; - geom.set_srid(3857); - et.from_geometry_if_3857(geom, expire_config); - } - auto const tiles = et.get_tiles(); REQUIRE(tiles.size() == 17); @@ -476,18 +462,8 @@ TEST_CASE("expire geometry collection", "[NoDB]") collection.add_geometry(geom::geometry_t{ geom::linestring_t{{15000.0, 15000.0}, {25000.0, 15000.0}}}); - SECTION("geom") - { - geom::geometry_t const geom{std::move(collection)}; - et.from_geometry(geom, expire_config); - } - - SECTION("geom with check") - { - geom::geometry_t geom{std::move(collection)}; - geom.set_srid(3857); - et.from_geometry_if_3857(geom, expire_config); - } + geom::geometry_t const geom{std::move(collection)}; + et.from_geometry(geom, expire_config); auto const tiles = et.get_tiles(); REQUIRE(tiles.size() == 6); @@ -499,6 +475,19 @@ TEST_CASE("expire geometry collection", "[NoDB]") CHECK(tile_t::from_quadkey(tiles[5], zoom) == tile_t{zoom, 2048, 2048}); } +TEST_CASE("expire works if in 3857", "[NoDB]") +{ + expire_config_t const expire_config; + expire_tiles et{zoom, defproj}; + + geom::geometry_t geom{geom::point_t{0.0, 0.0}}; + geom.set_srid(3857); + et.from_geometry_if_3857(geom, expire_config); + + auto const tiles = et.get_tiles(); + REQUIRE(tiles.size() == 4); +} + TEST_CASE("expire doesn't do anything if not in 3857", "[NoDB]") { expire_config_t const expire_config; From 22e3f9c9f583da5f1c6f2e9d717059d922445c74 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Tue, 22 Aug 2023 21:47:53 +0200 Subject: [PATCH 005/430] Github action: Test with PostgreSQL 15 instead of 14 --- .github/workflows/ci.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83e6f0581..d5ba9ee83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,7 +196,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu20-pg13-clang10-cpp17: + ubuntu20-pg15-clang10: runs-on: ubuntu-20.04 env: @@ -204,7 +204,7 @@ jobs: CXX: clang++-10 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 USE_PROJ_LIB: 6 BUILD_TYPE: Debug @@ -249,7 +249,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg14-clang14-jit: + ubuntu22-pg15-clang14-jit: runs-on: ubuntu-22.04 env: @@ -257,7 +257,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.4 LUAJIT_OPTION: ON - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 BUILD_TYPE: Debug PSYCOPG: 2 @@ -267,7 +267,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg14-clang14-proj6: + ubuntu22-pg15-clang14-proj6: runs-on: ubuntu-22.04 env: @@ -275,7 +275,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.4 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 USE_PROJ_LIB: 6 BUILD_TYPE: Debug @@ -286,7 +286,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg14-clang14-noproj: + ubuntu22-pg15-clang14-noproj: runs-on: ubuntu-22.04 env: @@ -294,7 +294,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 USE_PROJ_LIB: off BUILD_TYPE: Debug @@ -305,7 +305,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg14-clang14-cpp17: + ubuntu22-pg15-clang14: runs-on: ubuntu-22.04 env: @@ -313,7 +313,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.4 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 USE_PROJ_LIB: 6 BUILD_TYPE: Debug @@ -324,7 +324,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg14-gcc12-release: + ubuntu22-pg15-gcc12-release: runs-on: ubuntu-22.04 env: @@ -333,7 +333,7 @@ jobs: EXTRA_FLAGS: -Wno-stringop-overread LUA_VERSION: 5.4 LUAJIT_OPTION: ON - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 BUILD_TYPE: Release PSYCOPG: 2 @@ -343,14 +343,14 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg14-gcc12-release-nolua: + ubuntu22-pg15-gcc12-release-nolua: runs-on: ubuntu-22.04 env: CC: gcc-12 CXX: g++-12 EXTRA_FLAGS: -Wno-stringop-overread - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 BUILD_TYPE: Release PSYCOPG: 2 @@ -360,7 +360,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg14-clang14-cpp20: + ubuntu22-pg15-clang14-cpp20: runs-on: ubuntu-22.04 env: @@ -368,7 +368,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 CPP_VERSION: 20 BUILD_TYPE: Debug @@ -379,7 +379,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg14-gcc12-cpp20: + ubuntu22-pg15-gcc12-cpp20: runs-on: ubuntu-22.04 env: @@ -387,7 +387,7 @@ jobs: CXX: g++-12 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 14 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 CPP_VERSION: 20 BUILD_TYPE: Debug From b3109d9c77f7d3d7f83dcf2ca397397654157ae4 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Thu, 24 Aug 2023 15:11:47 +0200 Subject: [PATCH 006/430] Use colon syntax consistently when calling Lua functions on objects --- flex-config/bbox.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flex-config/bbox.lua b/flex-config/bbox.lua index 02b974c7b..920772859 100644 --- a/flex-config/bbox.lua +++ b/flex-config/bbox.lua @@ -82,7 +82,7 @@ end -- Format the bounding box we get from calling get_bbox() on the parameter -- in the way needed for the PostgreSQL/PostGIS box2d type. local function format_bbox(object) - local xmin, ymin, xmax, ymax = object.get_bbox() + local xmin, ymin, xmax, ymax = object:get_bbox() if xmin == nil then return nil end From 58dca4f253cad822a67abe3d5ed7967e40ff6b08 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sun, 27 Aug 2023 16:50:29 +0200 Subject: [PATCH 007/430] Revert "Refactor code that gets a mutex and calls into Lua code" This reverts commit 4dca25541a585e61d58e7682dadf0269603f5573. The select_relation_members() reads data from the Lua stack after calling call_lua_function(). It is important that this is still protected by the mutex. --- src/output-flex.cpp | 29 +++++++++++++++++++---------- src/output-flex.hpp | 7 +++++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/output-flex.cpp b/src/output-flex.cpp index 561b47a9a..bfbc73b6a 100644 --- a/src/output-flex.cpp +++ b/src/output-flex.cpp @@ -48,6 +48,9 @@ #include #include +// Mutex used to coordinate access to Lua code +static std::mutex lua_mutex; + // Lua can't call functions on C++ objects directly. This macro defines simple // C "trampoline" functions which are called from Lua which get the current // context (the output_flex_t object) and call the respective function on the @@ -895,9 +898,6 @@ int output_flex_t::expire_output_table() void output_flex_t::call_lua_function(prepared_lua_function_t func, osmium::OSMObject const &object) { - static std::mutex lua_mutex; - std::lock_guard const guard{lua_mutex}; - m_calling_context = func.context(); lua_pushvalue(lua_state(), func.index()); // the function to call @@ -914,6 +914,13 @@ void output_flex_t::call_lua_function(prepared_lua_function_t func, m_calling_context = calling_context::main; } +void output_flex_t::get_mutex_and_call_lua_function( + prepared_lua_function_t func, osmium::OSMObject const &object) +{ + std::lock_guard const guard{lua_mutex}; + call_lua_function(func, object); +} + void output_flex_t::pending_way(osmid_t id) { if (!m_process_way) { @@ -926,7 +933,7 @@ void output_flex_t::pending_way(osmid_t id) way_delete(id); - call_lua_function(m_process_way, m_way_cache.get()); + get_mutex_and_call_lua_function(m_process_way, m_way_cache.get()); } void output_flex_t::select_relation_members() @@ -935,6 +942,7 @@ void output_flex_t::select_relation_members() return; } + std::lock_guard const guard{lua_mutex}; call_lua_function(m_select_relation_members, m_relation_cache.get()); // If the function returned nil there is nothing to be marked. @@ -1014,7 +1022,8 @@ void output_flex_t::pending_relation(osmid_t id) delete_from_tables(osmium::item_type::relation, id); if (m_process_relation) { - call_lua_function(m_process_relation, m_relation_cache.get()); + get_mutex_and_call_lua_function(m_process_relation, + m_relation_cache.get()); } } @@ -1029,7 +1038,7 @@ void output_flex_t::pending_relation_stage1c(osmid_t id) } m_disable_add_row = true; - call_lua_function(m_process_relation, m_relation_cache.get()); + get_mutex_and_call_lua_function(m_process_relation, m_relation_cache.get()); m_disable_add_row = false; } @@ -1104,7 +1113,7 @@ void output_flex_t::node_add(osmium::Node const &node) } m_context_node = &node; - call_lua_function(m_process_node, node); + get_mutex_and_call_lua_function(m_process_node, node); m_context_node = nullptr; } @@ -1117,7 +1126,7 @@ void output_flex_t::way_add(osmium::Way *way) } m_way_cache.init(way); - call_lua_function(m_process_way, m_way_cache.get()); + get_mutex_and_call_lua_function(m_process_way, m_way_cache.get()); } void output_flex_t::relation_add(osmium::Relation const &relation) @@ -1128,7 +1137,7 @@ void output_flex_t::relation_add(osmium::Relation const &relation) m_relation_cache.init(relation); select_relation_members(); - call_lua_function(m_process_relation, relation); + get_mutex_and_call_lua_function(m_process_relation, relation); } void output_flex_t::delete_from_table(table_connection_t *table_connection, @@ -1518,7 +1527,7 @@ void output_flex_t::reprocess_marked() } way_delete(id); if (m_process_way) { - call_lua_function(m_process_way, m_way_cache.get()); + get_mutex_and_call_lua_function(m_process_way, m_way_cache.get()); } } diff --git a/src/output-flex.hpp b/src/output-flex.hpp index ed457f9d5..436237b32 100644 --- a/src/output-flex.hpp +++ b/src/output-flex.hpp @@ -185,12 +185,15 @@ class output_flex_t : public output_t /** * Call a Lua function that was "prepared" earlier with the OSMObject - * as its only parameter. Uses a mutex internally to make access to the - * Lua environment thread safe. + * as its only parameter. */ void call_lua_function(prepared_lua_function_t func, osmium::OSMObject const &object); + /// Aquire the lua_mutex and the call `call_lua_function()`. + void get_mutex_and_call_lua_function(prepared_lua_function_t func, + osmium::OSMObject const &object); + void init_lua(std::string const &filename); // Get the flex table that is as first parameter on the Lua stack. From f3e7d6972b21b055af38dbb80e28f4cff8a55cf6 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sun, 27 Aug 2023 16:58:00 +0200 Subject: [PATCH 008/430] Add comment in the hope that we don't make this error again --- src/output-flex.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/output-flex.cpp b/src/output-flex.cpp index bfbc73b6a..24443e65e 100644 --- a/src/output-flex.cpp +++ b/src/output-flex.cpp @@ -942,6 +942,8 @@ void output_flex_t::select_relation_members() return; } + // We can not use get_mutex_and_call_lua_function() here, because we need + // the mutex to stick around as long as we are looking at the Lua stack. std::lock_guard const guard{lua_mutex}; call_lua_function(m_select_relation_members, m_relation_cache.get()); From 8bb8fca981daa4fed1c4e579b635a79f7998ebf9 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 28 Aug 2023 09:50:57 +0200 Subject: [PATCH 009/430] Disable the warning that cache is disabled in append mode In append mode we don't necessarily have the flat node setting from the command line, because it is read from the properties file. So this would give us an unnecessary and wrong warning. This is just a band aid though, a better fix would be to check after we have read the properties file. But that needs some larger changes I'll leave for when we refactor the command line parsing code. --- src/command-line-parser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command-line-parser.cpp b/src/command-line-parser.cpp index f303da0d7..79c1749f1 100644 --- a/src/command-line-parser.cpp +++ b/src/command-line-parser.cpp @@ -486,7 +486,7 @@ static void check_options(options_t *options) throw std::runtime_error{ "RAM node cache can only be disabled in slim mode."}; } - if (options->flat_node_file.empty()) { + if (options->flat_node_file.empty() && !options->append) { log_warn("RAM cache is disabled. This will likely slow down " "processing a lot."); } From 71a1df7858f3d2d198f9b5d94815159f9f8b1ae2 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Tue, 29 Aug 2023 10:52:25 +0200 Subject: [PATCH 010/430] Generalization: Allow list of SQL commands in run_sql() command The run_sql() command now can have either a single SQL statement in the `sql` field or a list of SQL commands. The new `transaction` field can be set to `true` to wrap the SQL commands in BEGIN/COMMIT. --- src/gen/osm2pgsql-gen.cpp | 44 ++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index 64369585c..638b26109 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -368,15 +368,49 @@ class genproc_t std::string const description = luaX_get_table_string(lua_state(), "description", 1, "Argument #1"); - std::string const sql = - luaX_get_table_string(lua_state(), "sql", 1, "Argument #1"); - log_debug("Running SQL command: {}.", description); + bool const transaction = luaX_get_table_bool(lua_state(), "transaction", + 1, "Argument #1", false); + + std::vector queries; + if (transaction) { + queries.emplace_back("BEGIN"); + } + + lua_getfield(lua_state(), 1, "sql"); + int const ltype = lua_type(lua_state(), -1); + if (ltype == LUA_TSTRING) { + queries.emplace_back(lua_tostring(lua_state(), -1)); + } else if (ltype == LUA_TTABLE) { + if (!luaX_is_array(lua_state())) { + throw std::runtime_error{ + "Table in 'sql' field must be an array."}; + } + luaX_for_each(lua_state(), [&]() { + if (lua_type(lua_state(), -1) != LUA_TSTRING) { + throw std::runtime_error{ + "Table in 'sql' field must only contain strings."}; + } + queries.emplace_back(lua_tostring(lua_state(), -1)); + }); + } else { + throw std::runtime_error{ + "Argument #1 must contain a 'sql' string or table field."}; + } + + if (transaction) { + queries.emplace_back("COMMIT"); + } + + log_debug("Running SQL commands: {}.", description); util::timer_t timer_sql; pg_conn_t const db_connection{m_conninfo}; - db_connection.exec(sql); - log_debug("SQL command took {}.", + for (auto const &query : queries) { + log_debug("Running sql: {}", query); + db_connection.exec(query); + } + log_debug("SQL commands took {}.", util::human_readable_duration(timer_sql.stop())); return 0; From bdd64445d6c20c9f6f0557a6d64da0db1a3aaff2 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 28 Aug 2023 10:03:01 +0200 Subject: [PATCH 011/430] Refactor: Build a list of (templated) queries, then run them --- src/middle-pgsql.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/middle-pgsql.cpp b/src/middle-pgsql.cpp index a66c17acf..0c8d6fc0f 100644 --- a/src/middle-pgsql.cpp +++ b/src/middle-pgsql.cpp @@ -822,13 +822,15 @@ void middle_pgsql_t::get_node_parents( send_id_list(m_db_connection, "osm2pgsql_changed_nodes", changed_nodes); - m_db_connection.exec("ANALYZE osm2pgsql_changed_nodes"); + std::vector queries; + + queries.emplace_back("ANALYZE osm2pgsql_changed_nodes"); bool const has_bucket_index = check_bucket_index(&m_db_connection, m_options->prefix); if (has_bucket_index) { - m_db_connection.exec(build_sql(*m_options, R"( + queries.emplace_back(R"( WITH changed_buckets AS ( SELECT array_agg(id) AS node_ids, id >> {way_node_index_id_shift} AS bucket FROM osm2pgsql_changed_nodes GROUP BY id >> {way_node_index_id_shift} @@ -839,31 +841,35 @@ INSERT INTO osm2pgsql_changed_ways WHERE w.nodes && b.node_ids AND {schema}"{prefix}_index_bucket"(w.nodes) && ARRAY[b.bucket]; - )")); + )"); } else { - m_db_connection.exec(build_sql(*m_options, R"( + queries.emplace_back(R"( INSERT INTO osm2pgsql_changed_ways SELECT DISTINCT w.id FROM {schema}"{prefix}_ways" w, osm2pgsql_changed_nodes n WHERE w.nodes && ARRAY[n.id] - )")); + )"); } if (m_options->middle_database_format == 1) { - m_db_connection.exec(build_sql(*m_options, R"( + queries.emplace_back(R"( INSERT INTO osm2pgsql_changed_relations SELECT DISTINCT r.id FROM {schema}"{prefix}_rels" r, osm2pgsql_changed_nodes n WHERE r.parts && ARRAY[n.id] AND r.parts[1:way_off] && ARRAY[n.id] - )")); + )"); } else { - m_db_connection.exec(build_sql(*m_options, R"( + queries.emplace_back(R"( INSERT INTO osm2pgsql_changed_relations SELECT DISTINCT r.id FROM {schema}"{prefix}_rels" r, osm2pgsql_changed_nodes c WHERE {schema}"{prefix}_member_ids"(r.members, 'N'::char) && ARRAY[c.id]; - )")); + )"); + } + + for (auto const &query : queries) { + m_db_connection.exec(build_sql(*m_options, query)); } load_id_list(m_db_connection, "osm2pgsql_changed_ways", parent_ways); From 29ff7eeb2d7f9901b2ffec955ac8cd4cb0cf2c2b Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 28 Aug 2023 10:05:01 +0200 Subject: [PATCH 012/430] Move DISTINCT from queries generating list to when we read the data We had some problems with the query planner getting confused with those queries. Lets make them as simple as possible to not confuse it unnecessarily. This might be vodoo, but it doesn't cost much, just a little bit of temporary space in the database, because when reading we do the ORDER BY anyway, so the extra DISTINCT isn't more cost. --- src/middle-pgsql.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/middle-pgsql.cpp b/src/middle-pgsql.cpp index 0c8d6fc0f..7ca140b6c 100644 --- a/src/middle-pgsql.cpp +++ b/src/middle-pgsql.cpp @@ -73,8 +73,8 @@ static void load_id_list(pg_conn_t const &db_connection, std::string const &table, osmium::index::IdSetSmall *ids) { - auto const res = - db_connection.exec(fmt::format("SELECT id FROM {} ORDER BY id", table)); + auto const res = db_connection.exec( + fmt::format("SELECT DISTINCT id FROM {} ORDER BY id", table)); for (int n = 0; n < res.num_tuples(); ++n) { ids->set(osmium::string_to_object_id(res.get_value(n, 0))); } @@ -836,7 +836,7 @@ WITH changed_buckets AS ( FROM osm2pgsql_changed_nodes GROUP BY id >> {way_node_index_id_shift} ) INSERT INTO osm2pgsql_changed_ways - SELECT DISTINCT w.id + SELECT w.id FROM {schema}"{prefix}_ways" w, changed_buckets b WHERE w.nodes && b.node_ids AND {schema}"{prefix}_index_bucket"(w.nodes) @@ -845,7 +845,7 @@ INSERT INTO osm2pgsql_changed_ways } else { queries.emplace_back(R"( INSERT INTO osm2pgsql_changed_ways - SELECT DISTINCT w.id + SELECT w.id FROM {schema}"{prefix}_ways" w, osm2pgsql_changed_nodes n WHERE w.nodes && ARRAY[n.id] )"); @@ -854,7 +854,7 @@ INSERT INTO osm2pgsql_changed_ways if (m_options->middle_database_format == 1) { queries.emplace_back(R"( INSERT INTO osm2pgsql_changed_relations - SELECT DISTINCT r.id + SELECT r.id FROM {schema}"{prefix}_rels" r, osm2pgsql_changed_nodes n WHERE r.parts && ARRAY[n.id] AND r.parts[1:way_off] && ARRAY[n.id] @@ -862,7 +862,7 @@ INSERT INTO osm2pgsql_changed_relations } else { queries.emplace_back(R"( INSERT INTO osm2pgsql_changed_relations - SELECT DISTINCT r.id + SELECT r.id FROM {schema}"{prefix}_rels" r, osm2pgsql_changed_nodes c WHERE {schema}"{prefix}_member_ids"(r.members, 'N'::char) && ARRAY[c.id]; )"); From 556c8e8772fe53655c663db1d31f8780d6473cb4 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 28 Aug 2023 11:54:21 +0200 Subject: [PATCH 013/430] Put query for finding parent ways from nodes inside a PL/pgSQL function The query we had before would sometimes result in really bad performance when the query planner in PostgreQL decides that it needs to use sequential scans on the ways table. This is due to the statistics being way off. By running the query for each node ids on its own we hopefully trick the query planner into always using an index lookup. See #2056 --- src/middle-pgsql.cpp | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/middle-pgsql.cpp b/src/middle-pgsql.cpp index 7ca140b6c..45ef7fdf8 100644 --- a/src/middle-pgsql.cpp +++ b/src/middle-pgsql.cpp @@ -830,18 +830,33 @@ void middle_pgsql_t::get_node_parents( check_bucket_index(&m_db_connection, m_options->prefix); if (has_bucket_index) { + // The query to get the parent ways of changed nodes is "hidden" + // inside a PL/pgSQL function so that the query planner only sees + // a single node id that is being queried for. If we ask for all + // nodes at the same time the query planner sometimes thinks it is + // better to do a full table scan which totally destroys performance. + // This is due to the PostgreSQL statistics on ARRAYs being way off. queries.emplace_back(R"( -WITH changed_buckets AS ( - SELECT array_agg(id) AS node_ids, id >> {way_node_index_id_shift} AS bucket - FROM osm2pgsql_changed_nodes GROUP BY id >> {way_node_index_id_shift} -) -INSERT INTO osm2pgsql_changed_ways - SELECT w.id - FROM {schema}"{prefix}_ways" w, changed_buckets b - WHERE w.nodes && b.node_ids - AND {schema}"{prefix}_index_bucket"(w.nodes) - && ARRAY[b.bucket]; - )"); +CREATE OR REPLACE FUNCTION osm2pgsql_find_changed_ways() RETURNS void AS $$ +DECLARE + changed_buckets RECORD; +BEGIN + FOR changed_buckets IN + SELECT array_agg(id) AS node_ids, id >> {way_node_index_id_shift} AS bucket + FROM osm2pgsql_changed_nodes GROUP BY id >> {way_node_index_id_shift} + LOOP + INSERT INTO osm2pgsql_changed_ways + SELECT DISTINCT w.id + FROM {schema}"{prefix}_ways" w + WHERE w.nodes && changed_buckets.node_ids + AND {schema}"{prefix}_index_bucket"(w.nodes) + && ARRAY[changed_buckets.bucket]; + END LOOP; +END; +$$ LANGUAGE plpgsql +)"); + queries.emplace_back("SELECT osm2pgsql_find_changed_ways()"); + queries.emplace_back("DROP FUNCTION osm2pgsql_find_changed_ways()"); } else { queries.emplace_back(R"( INSERT INTO osm2pgsql_changed_ways From fdddd610ba758bef6d9b73d734cb3104f4a8b6bc Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Tue, 29 Aug 2023 11:51:51 +0200 Subject: [PATCH 014/430] Generalizer: Show some more information in log level info Now we can see SQL queries and generalizers run with some timing info already in log level info. --- src/gen/osm2pgsql-gen.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index 638b26109..b350c5df1 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -296,7 +296,7 @@ class genproc_t int app_run_gen() { - log_debug("Running configured generalizer (run {})...", ++m_gen_run); + log_debug("Configuring generalizer..."); if (lua_type(lua_state(), 1) != LUA_TSTRING) { throw std::runtime_error{"Argument #1 to 'run_gen' must be a " @@ -326,8 +326,8 @@ class genproc_t auto generalizer = create_generalizer(strategy, &db_connection, ¶ms); - log_debug("Generalizer '{}' ({}) initialized.", generalizer->name(), - generalizer->strategy()); + log_info("Running generalizer '{}' ({})...", generalizer->name(), + generalizer->strategy()); if (m_append) { params.set("delete_existing", true); @@ -335,6 +335,7 @@ class genproc_t write_to_debug_log(params, "Params (after initialization):"); + util::timer_t timer_gen; if (generalizer->on_tiles()) { process_tiles(db_connection, params, generalizer.get()); } else { @@ -353,8 +354,8 @@ class genproc_t std::chrono::duration_cast( timer.elapsed()))); } - log_debug("Finished generalizer '{}' (run {}).", generalizer->name(), - m_gen_run); + log_info("Finished generalizer '{}' in {}.", generalizer->name(), + util::human_readable_duration(timer_gen.stop())); return 0; } @@ -402,7 +403,7 @@ class genproc_t queries.emplace_back("COMMIT"); } - log_debug("Running SQL commands: {}.", description); + log_info("Running SQL commands: {}.", description); util::timer_t timer_sql; pg_conn_t const db_connection{m_conninfo}; @@ -410,8 +411,8 @@ class genproc_t log_debug("Running sql: {}", query); db_connection.exec(query); } - log_debug("SQL commands took {}.", - util::human_readable_duration(timer_sql.stop())); + log_info("Finished SQL commands in {}.", + util::human_readable_duration(timer_sql.stop())); return 0; } @@ -538,7 +539,6 @@ class genproc_t std::string m_conninfo; std::string m_dbschema; - std::size_t m_gen_run = 0; uint32_t m_jobs; bool m_append; bool m_updatable; From 0798a51949452c353119bb430274ff608e4bb8ac Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sat, 2 Sep 2023 11:07:28 +0200 Subject: [PATCH 015/430] Clean tests: Remove 'name' field from expire which is never used This is a left-over from earlier use of the 'name' field. --- tests/bdd/flex/lua-expire-output-definitions.feature | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/bdd/flex/lua-expire-output-definitions.feature b/tests/bdd/flex/lua-expire-output-definitions.feature index fbebf9fa0..bb1c74c6b 100644 --- a/tests/bdd/flex/lua-expire-output-definitions.feature +++ b/tests/bdd/flex/lua-expire-output-definitions.feature @@ -17,7 +17,6 @@ Feature: Expire output definitions in Lua file And the lua style """ osm2pgsql.define_expire_output({ - name = 'foo', filename = false }) """ @@ -32,7 +31,6 @@ Feature: Expire output definitions in Lua file And the lua style """ osm2pgsql.define_expire_output({ - name = 'foo', table = 'bar', schema = false }) @@ -48,7 +46,6 @@ Feature: Expire output definitions in Lua file And the lua style """ osm2pgsql.define_expire_output({ - name = 'foo', table = false }) """ @@ -63,7 +60,6 @@ Feature: Expire output definitions in Lua file And the lua style """ osm2pgsql.define_expire_output({ - name = 'foo', maxzoom = 'bar', filename = 'somewhere' }) @@ -79,7 +75,6 @@ Feature: Expire output definitions in Lua file And the lua style """ osm2pgsql.define_expire_output({ - name = 'foo', maxzoom = 12, minzoom = 'bar', filename = 'somewhere' @@ -96,7 +91,6 @@ Feature: Expire output definitions in Lua file And the lua style """ osm2pgsql.define_expire_output({ - name = 'foo', maxzoom = 123, filename = 'somewhere' }) @@ -112,7 +106,6 @@ Feature: Expire output definitions in Lua file And the lua style """ osm2pgsql.define_expire_output({ - name = 'foo', maxzoom = 12, minzoom = -3, filename = 'somewhere' @@ -129,7 +122,6 @@ Feature: Expire output definitions in Lua file And the lua style """ osm2pgsql.define_expire_output({ - name = 'foo', maxzoom = 12, minzoom = 14, filename = 'somewhere' From 4f6f2af39fdd8e6458c9a1ea9d20f3cff72310c9 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sat, 2 Sep 2023 21:59:18 +0200 Subject: [PATCH 016/430] Actually add geometries in the test Instead of just NULL geometries to make them a bit more realistic. --- tests/bdd/flex/lua-expire.feature | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/bdd/flex/lua-expire.feature b/tests/bdd/flex/lua-expire.feature index 186d4095b..6998c8684 100644 --- a/tests/bdd/flex/lua-expire.feature +++ b/tests/bdd/flex/lua-expire.feature @@ -72,7 +72,7 @@ Feature: Expire configuration in Lua file }) function osm2pgsql.process_node(object) - t:insert({}) + t:insert({ some = object:as_point() }) end """ When running osm2pgsql flex @@ -93,7 +93,7 @@ Feature: Expire configuration in Lua file }) function osm2pgsql.process_node(object) - t:insert({}) + t:insert({ some = object:as_point() }) end """ When running osm2pgsql flex @@ -136,7 +136,7 @@ Feature: Expire configuration in Lua file }) function osm2pgsql.process_node(object) - t:insert({}) + t:insert({ some = object:as_point() }) end """ Then running osm2pgsql flex fails @@ -184,7 +184,7 @@ Feature: Expire configuration in Lua file }) function osm2pgsql.process_node(object) - t:insert({}) + t:insert({ some = object:as_point() }) end """ When running osm2pgsql flex @@ -208,7 +208,7 @@ Feature: Expire configuration in Lua file }) function osm2pgsql.process_node(object) - t:insert({}) + t:insert({ some = object:as_point() }) end """ When running osm2pgsql flex @@ -231,7 +231,7 @@ Feature: Expire configuration in Lua file }) function osm2pgsql.process_node(object) - t:insert({}) + t:insert({ geom = object:as_point() }) end """ When running osm2pgsql flex From b45e2c418528b8589487c765ffc365eb73c2e07e Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Fri, 1 Sep 2023 09:59:50 +0200 Subject: [PATCH 017/430] Faster hex decoder --- src/gen/gen-rivers.cpp | 2 +- src/gen/gen-tile-builtup.cpp | 2 +- src/gen/gen-tile-raster.cpp | 2 +- src/wkb.cpp | 41 +++++++++++++++++++----------------- src/wkb.hpp | 8 +++---- tests/test-wkb.cpp | 13 ++++++------ 6 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/gen/gen-rivers.cpp b/src/gen/gen-rivers.cpp index eeb59c177..096661d18 100644 --- a/src/gen/gen-rivers.cpp +++ b/src/gen/gen-rivers.cpp @@ -258,7 +258,7 @@ SELECT "{id_column}", "{width_column}", "{name_column}", "{geom_column}" if (!name.empty()) { names.emplace(id, name); } - auto const geom = ewkb_to_geom(decode_hex(result.get_value(i, 3))); + auto const geom = ewkb_to_geom(decode_hex(result.get(i, 3))); if (geom.is_linestring()) { auto const &ls = geom.get(); diff --git a/src/gen/gen-tile-builtup.cpp b/src/gen/gen-tile-builtup.cpp index a1190b1c8..6992b030c 100644 --- a/src/gen/gen-tile-builtup.cpp +++ b/src/gen/gen-tile-builtup.cpp @@ -173,7 +173,7 @@ static void draw_from_db(double margin, canvas_list_t *canvas_list, box.max_x(), box.max_y()); for (int n = 0; n < result.num_tuples(); ++n) { - auto const geom = ewkb_to_geom(decode_hex(result.get_value(n, 0))); + auto const geom = ewkb_to_geom(decode_hex(result.get(n, 0))); cc.canvas.draw(geom, tile); } } diff --git a/src/gen/gen-tile-raster.cpp b/src/gen/gen-tile-raster.cpp index 3c9019cd8..6ac7ba0db 100644 --- a/src/gen/gen-tile-raster.cpp +++ b/src/gen/gen-tile-raster.cpp @@ -163,7 +163,7 @@ static void draw_from_db(double margin, unsigned int image_extent, for (int n = 0; n < result.num_tuples(); ++n) { std::string param = result.get_value(n, 1); - auto const geom = ewkb_to_geom(decode_hex(result.get_value(n, 0))); + auto const geom = ewkb_to_geom(decode_hex(result.get(n, 0))); auto const [it, success] = canvas_list->try_emplace( std::move(param), image_extent, image_buffer); diff --git a/src/wkb.cpp b/src/wkb.cpp index a55d5493b..cf888d500 100644 --- a/src/wkb.cpp +++ b/src/wkb.cpp @@ -576,32 +576,35 @@ geom::geometry_t ewkb_to_geom(std::string_view wkb) return geom; } -unsigned char decode_hex_char(char c) -{ - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'A' && c <= 'F') { - return c - 'A' + 10; - } - if (c >= 'a' && c <= 'f') { - return c - 'a' + 10; - } +static constexpr std::array const hex_table = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, + + 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; - throw std::runtime_error{"Invalid wkb: Not a hex character"}; +unsigned char decode_hex_char(char c) noexcept +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) + return hex_table[static_cast(static_cast(c))]; } -std::string decode_hex(char const *hex) +std::string decode_hex(std::string_view hex_string) { + if (hex_string.size() % 2 != 0) { + throw std::runtime_error{"Invalid wkb: Not a valid hex string"}; + } + std::string wkb; + wkb.reserve(hex_string.size() / 2); - while (*hex != '\0') { + // NOLINTNEXTLINE(llvm-qualified-auto, readability-qualified-auto) + for (auto hex = hex_string.begin(); hex != hex_string.end();) { unsigned int const c = decode_hex_char(*hex++); - - if (*hex == '\0') { - throw std::runtime_error{"Invalid wkb: Not a valid hex string"}; - } - wkb += static_cast((c << 4U) | decode_hex_char(*hex++)); } diff --git a/src/wkb.hpp b/src/wkb.hpp index 24a46098a..c76acd6a5 100644 --- a/src/wkb.hpp +++ b/src/wkb.hpp @@ -41,15 +41,15 @@ [[nodiscard]] geom::geometry_t ewkb_to_geom(std::string_view wkb); /** - * Decode one hex character (0-9A-F or 0-9a-f) and return its value. Throw - * an exception if not a valid hex character. + * Decode one hex character (0-9A-F or 0-9a-f) and return its value. + * Returns 0 for characters that are not hex characters. */ -[[nodiscard]] unsigned char decode_hex_char(char c); +[[nodiscard]] unsigned char decode_hex_char(char c) noexcept; /** * Decode a string of hex characters. Throws an exception if the input is not * a valid hex encoding. */ -[[nodiscard]] std::string decode_hex(char const *hex); +[[nodiscard]] std::string decode_hex(std::string_view hex); #endif // OSM2PGSQL_WKB_HPP diff --git a/tests/test-wkb.cpp b/tests/test-wkb.cpp index 538081153..816ad9cf5 100644 --- a/tests/test-wkb.cpp +++ b/tests/test-wkb.cpp @@ -189,7 +189,7 @@ TEST_CASE("wkb: geometrycollection", "[NoDB]") TEST_CASE("wkb: invalid", "[NoDB]") { REQUIRE_THROWS(ewkb_to_geom("INVALID")); } -TEST_CASE("wkb hex decode of valid hex characters") +TEST_CASE("wkb hex decode of valid and invalid hex characters") { REQUIRE(decode_hex_char('0') == 0); REQUIRE(decode_hex_char('9') == 9); @@ -197,7 +197,11 @@ TEST_CASE("wkb hex decode of valid hex characters") REQUIRE(decode_hex_char('f') == 0x0f); REQUIRE(decode_hex_char('A') == 0x0a); REQUIRE(decode_hex_char('F') == 0x0f); - REQUIRE_THROWS(decode_hex_char('x')); + REQUIRE(decode_hex_char('#') == 0); + REQUIRE(decode_hex_char('@') == 0); + REQUIRE(decode_hex_char('g') == 0); + REQUIRE(decode_hex_char('G') == 0); + REQUIRE(decode_hex_char(0x7f) == 0); } TEST_CASE("wkb hex decode of valid hex string") @@ -216,11 +220,6 @@ TEST_CASE("wkb hex decode of valid hex string") REQUIRE(result == data); } -TEST_CASE("wkb hex decode of invalid hex string") -{ - REQUIRE_THROWS(decode_hex("no")); -} - TEST_CASE("wkb hex decode of empty string is okay") { std::string const hex{}; From 422ba14490b33deba5d0ddf1aae02ebe9aa72676 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Fri, 1 Sep 2023 11:47:16 +0200 Subject: [PATCH 018/430] Better implementation for low-level bit-fiddling in tracer code --- src/gen/tracer.cpp | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/gen/tracer.cpp b/src/gen/tracer.cpp index 5321c3eae..62cd37905 100644 --- a/src/gen/tracer.cpp +++ b/src/gen/tracer.cpp @@ -41,6 +41,15 @@ void tracer_t::reset() m_num_points = 0; } +static potrace_word bit_squeeze(potrace_word w, unsigned char const *d) noexcept +{ + return (0x80U & d[0]) | (0x40U & d[1]) | (0x20U & d[2]) | (0x10U & d[3]) | + (0x08U & d[4]) | (0x04U & d[5]) | (0x02U & d[6]) | (0x01U & d[7]) | + w; +} + +static_assert(sizeof(potrace_word) == 8); + void tracer_t::prepare(canvas_t const &canvas) noexcept { std::size_t const size = canvas.size(); @@ -48,14 +57,17 @@ void tracer_t::prepare(canvas_t const &canvas) noexcept m_bits.reserve((size * size) / bits_per_word); - unsigned char const *d = canvas.begin(); - while (d != canvas.end()) { - potrace_word w = 0x1U & *d++; - for (std::size_t n = 1; n < bits_per_word; ++n) { - w <<= 1U; - assert(d != canvas.end()); - w |= 0x1U & *d++; - } + for (unsigned char const *d = canvas.begin(); d != canvas.end(); d += 8) { + auto w = bit_squeeze(0, d); + + w = bit_squeeze(w << 8U, d += 8); + w = bit_squeeze(w << 8U, d += 8); + w = bit_squeeze(w << 8U, d += 8); + w = bit_squeeze(w << 8U, d += 8); + w = bit_squeeze(w << 8U, d += 8); + w = bit_squeeze(w << 8U, d += 8); + w = bit_squeeze(w << 8U, d += 8); + m_bits.push_back(w); } From 30af427c6a50e9bb0ca0d9f55d498a6ea563f418 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sun, 3 Sep 2023 11:01:31 +0200 Subject: [PATCH 019/430] Generalization: Allow setting condition for running SQL commands If a condition is set with "if_has_rows", the SQL command in "if_has_rows" will be run and only if that returns any rows, the main sql command is run. This check happens only in "append" mode, in "create" mode the command is always run. To be used with something like "SELECT 1 FROM some_table LIMIT 1" to check whether the table has any rows (for instance an expire table). --- src/gen/osm2pgsql-gen.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index b350c5df1..e892dd16c 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -373,6 +373,9 @@ class genproc_t bool const transaction = luaX_get_table_bool(lua_state(), "transaction", 1, "Argument #1", false); + std::string const if_has_rows = luaX_get_table_string( + lua_state(), "if_has_rows", 1, "Argument #1", ""); + std::vector queries; if (transaction) { queries.emplace_back("BEGIN"); @@ -403,10 +406,21 @@ class genproc_t queries.emplace_back("COMMIT"); } + pg_conn_t const db_connection{m_conninfo}; + + if (m_append && !if_has_rows.empty()) { + auto const result = db_connection.exec(if_has_rows); + if (result.num_tuples() == 0) { + log_info("Not running SQL command: {} (no rows in " + "condition result).", + description); + return 0; + } + } + log_info("Running SQL commands: {}.", description); util::timer_t timer_sql; - pg_conn_t const db_connection{m_conninfo}; for (auto const &query : queries) { log_debug("Running sql: {}", query); db_connection.exec(query); From 415de9abdf2d003a5c0a0abe8e8fc139acacc2b5 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Mon, 4 Sep 2023 10:57:53 +0200 Subject: [PATCH 020/430] prepare release 1.9.2 --- CMakeLists.txt | 2 +- docs/osm2pgsql-replication.1 | 2 +- docs/osm2pgsql.1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f6b51798..f5195612d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.8.0) -project(osm2pgsql VERSION 1.9.1 LANGUAGES CXX C) +project(osm2pgsql VERSION 1.9.2 LANGUAGES CXX C) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/docs/osm2pgsql-replication.1 b/docs/osm2pgsql-replication.1 index 5d39bdad9..3d1109b03 100644 --- a/docs/osm2pgsql-replication.1 +++ b/docs/osm2pgsql-replication.1 @@ -1,4 +1,4 @@ -.TH "OSM2PGSQL-REPLICATION" "1" "1.9.1" "" "" +.TH "OSM2PGSQL-REPLICATION" "1" "1.9.2" "" "" .SH NAME osm2pgsql-replication \- osm2pgsql database updater .SH SYNOPSIS diff --git a/docs/osm2pgsql.1 b/docs/osm2pgsql.1 index 9bf66a6fe..dd69f9556 100644 --- a/docs/osm2pgsql.1 +++ b/docs/osm2pgsql.1 @@ -1,4 +1,4 @@ -.TH "OSM2PGSQL" "1" "1.9.1" "" "" +.TH "OSM2PGSQL" "1" "1.9.2" "" "" .SH NAME .PP osm2pgsql - OpenStreetMap data to PostgreSQL converter From 389904269a1b1f246641d203abadfcd5cebb97cc Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 4 Sep 2023 17:26:11 +0200 Subject: [PATCH 021/430] Check in CMake config that we are building for 64bit architecture For a long time we haven't officially supported 32bit architectures but not really disabled them. But this is giving us more problems lately and we can't test on 32 bit systems anyway, so this now gives us an up front check. --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index f5195612d..34d2e7c77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,10 @@ else() set(ENABLE_INSTALL OFF) endif() +if (NOT ${CMAKE_SIZEOF_VOID_P} EQUAL 8) + message(FATAL_ERROR "osm2pgsql needs a 64 bit architecture") +endif() + if (WIN32) set(DEFAULT_STYLE "default.style" CACHE STRING "Default style used unless one is given on the command line") else() From 32389c1fe3cf3a8dafb0118058905361839d6577 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Tue, 22 Aug 2023 12:02:05 +0200 Subject: [PATCH 022/430] Add runtime warnings that area type and add_row() are deprecated Only one message about add_row() is shown. --- src/flex-table-column.cpp | 6 ++++++ src/output-flex.cpp | 7 +++++++ src/output-flex.hpp | 2 ++ 3 files changed, 15 insertions(+) diff --git a/src/flex-table-column.cpp b/src/flex-table-column.cpp index 91824e268..c0c4b9aa5 100644 --- a/src/flex-table-column.cpp +++ b/src/flex-table-column.cpp @@ -62,6 +62,12 @@ static table_column_type get_column_type_from_string(std::string const &type) throw fmt_error("Unknown column type '{}'.", type); } + if (column_type->type == table_column_type::area) { + log_warn("The 'area' column type is deprecated. Please read"); + log_warn("https://osm2pgsql.org/doc/tutorials/" + "switching-from-add-row-to-insert/"); + } + return column_type->type; } diff --git a/src/output-flex.cpp b/src/output-flex.cpp index 24443e65e..735bbe81a 100644 --- a/src/output-flex.cpp +++ b/src/output-flex.cpp @@ -587,6 +587,13 @@ int output_flex_t::table_add_row() } lua_remove(lua_state(), 1); + if (m_add_row_has_never_been_called) { + m_add_row_has_never_been_called = false; + log_warn("The add_row() function is deprecated. Please read"); + log_warn("https://osm2pgsql.org/doc/tutorials/" + "switching-from-add-row-to-insert/"); + } + if (m_calling_context == calling_context::process_node) { if (!table.matches_type(osmium::item_type::node)) { throw fmt_error("Trying to add node to table '{}'.", table.name()); diff --git a/src/output-flex.hpp b/src/output-flex.hpp index 436237b32..89cced9b2 100644 --- a/src/output-flex.hpp +++ b/src/output-flex.hpp @@ -316,6 +316,8 @@ class output_flex_t : public output_t * add_row() command. */ bool m_disable_add_row = false; + + bool m_add_row_has_never_been_called = true; }; #endif // OSM2PGSQL_OUTPUT_FLEX_HPP From a29a086291a718a9c13940a246d30e9bbd298e98 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Fri, 25 Aug 2023 08:59:28 +0200 Subject: [PATCH 023/430] Rename "docs" directory to "man" And updates various docs accordingly. For a while we haven't kept any docs in there other than the man pages. The new name better reflects what this directory is all about. --- CMakeLists.txt | 2 +- CONTRIBUTING.md | 12 ++++++------ docs/README.md | 11 ----------- {docs => man}/CMakeLists.txt | 0 {docs => man}/manpage.template | 0 {docs => man}/manpage_from_python.py | 0 {docs => man}/osm2pgsql-gen.1 | 0 {docs => man}/osm2pgsql-gen.md | 0 {docs => man}/osm2pgsql-replication.1 | 0 {docs => man}/osm2pgsql.1 | 0 {docs => man}/osm2pgsql.md | 0 style.lua | 3 ++- 12 files changed, 9 insertions(+), 19 deletions(-) delete mode 100644 docs/README.md rename {docs => man}/CMakeLists.txt (100%) rename {docs => man}/manpage.template (100%) rename {docs => man}/manpage_from_python.py (100%) rename {docs => man}/osm2pgsql-gen.1 (100%) rename {docs => man}/osm2pgsql-gen.md (100%) rename {docs => man}/osm2pgsql-replication.1 (100%) rename {docs => man}/osm2pgsql.1 (100%) rename {docs => man}/osm2pgsql.md (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 34d2e7c77..1c1f38de3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ endif() # Man page ############################################################# -add_subdirectory(docs) +add_subdirectory(man) ############################################################# # Install diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 496591089..921faf2c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,12 +55,12 @@ convention](https://www.doxygen.nl/manual/docblocks.html) using backslashes ## Documentation -User documentation is available on [the website](https://osm2pgsql.org/), some -is stored in `docs/`. Pages on the OpenStreetMap wiki are known to be -unreliable and outdated. +User documentation is available on [the website](https://osm2pgsql.org/). The +source of the web page is in its own repository +(https://github.com/openstreetmap/osm2pgsql-website). -The man pages for [osm2pgsql](docs/osm2pgsql.1) and -[osm2pgsql-replication](docs/osm2pgsql-replication.1) can be built from source +The man pages for [osm2pgsql](man/osm2pgsql.1) and +[osm2pgsql-replication](man/osm2pgsql-replication.1) can be built from source with `make man`. They need pandoc and argparse-manpage for the conversion. These tools can be @@ -207,7 +207,7 @@ the report. * Decide on a new version. (See [semantic versioning](https://semver.org/).) * Update version in [CMakeLists.txt](CMakeLists.txt), look for `project` function. -* Build man page (`make man`) and copy it to `docs/osm2pgsql.1`. +* Build man page (`make man`) and copy it to `man/osm2pgsql.1`. * Tag release with release notes in commit message and upload the tag to Github. * Fill out release notes on Github. * Copy Windows binaries and source tarball to osm2pgsql.org. diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 7a0049d12..000000000 --- a/docs/README.md +++ /dev/null @@ -1,11 +0,0 @@ - -# osm2pgsql Documentation - -This directory contains the [source code for the man page](osm2pgsql.md) and -the [compiled version](osm2pgsql.1) and the files necessary to create it. -Call `make man` to re-build the man page. - -The documentation that used to be in this directory has been moved to -the [osm2pgsql manual](https://osm2pgsql.org/doc/manual.html) or to the -[examples](https://osm2pgsql.org/examples) on the website. - diff --git a/docs/CMakeLists.txt b/man/CMakeLists.txt similarity index 100% rename from docs/CMakeLists.txt rename to man/CMakeLists.txt diff --git a/docs/manpage.template b/man/manpage.template similarity index 100% rename from docs/manpage.template rename to man/manpage.template diff --git a/docs/manpage_from_python.py b/man/manpage_from_python.py similarity index 100% rename from docs/manpage_from_python.py rename to man/manpage_from_python.py diff --git a/docs/osm2pgsql-gen.1 b/man/osm2pgsql-gen.1 similarity index 100% rename from docs/osm2pgsql-gen.1 rename to man/osm2pgsql-gen.1 diff --git a/docs/osm2pgsql-gen.md b/man/osm2pgsql-gen.md similarity index 100% rename from docs/osm2pgsql-gen.md rename to man/osm2pgsql-gen.md diff --git a/docs/osm2pgsql-replication.1 b/man/osm2pgsql-replication.1 similarity index 100% rename from docs/osm2pgsql-replication.1 rename to man/osm2pgsql-replication.1 diff --git a/docs/osm2pgsql.1 b/man/osm2pgsql.1 similarity index 100% rename from docs/osm2pgsql.1 rename to man/osm2pgsql.1 diff --git a/docs/osm2pgsql.md b/man/osm2pgsql.md similarity index 100% rename from docs/osm2pgsql.md rename to man/osm2pgsql.md diff --git a/style.lua b/style.lua index 8b0b88d66..792c92bba 100644 --- a/style.lua +++ b/style.lua @@ -1,4 +1,5 @@ --- For documentation of Lua tag transformations, see docs/lua.md. +-- For documentation of Lua tag transformations see +-- https://osm2pgsql.org/doc/manual.html#the-pgsql-output -- Objects with any of the following keys will be treated as polygon polygon_keys = { From 2d17341d0fbff12082b671bd75ee54741a0edf0a Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Tue, 5 Sep 2023 10:55:51 +0200 Subject: [PATCH 024/430] Github actions: Update checkout to v4 in workflow files --- .github/workflows/ci.yml | 44 +++++++++++++++--------------- .github/workflows/luacheck.yml | 2 +- .github/workflows/test-install.yml | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5ba9ee83..d0adf68fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: LUA_VERSION: 5.4 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install prerequisites run: | @@ -61,7 +61,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -79,7 +79,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -98,7 +98,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -117,7 +117,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -135,7 +135,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -154,7 +154,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -173,7 +173,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -192,7 +192,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -211,7 +211,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -229,7 +229,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -245,7 +245,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -263,7 +263,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -282,7 +282,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -301,7 +301,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -320,7 +320,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -339,7 +339,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -356,7 +356,7 @@ jobs: PSYCOPG: 2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -375,7 +375,7 @@ jobs: PSYCOPG: 3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -394,7 +394,7 @@ jobs: PSYCOPG: 3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test @@ -411,7 +411,7 @@ jobs: VCPKG_DEFAULT_BINARY_CACHE: C:/vcpkg_binary_cache steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/cache@v3 with: path: | @@ -456,7 +456,7 @@ jobs: OSMURL: https://download.geofabrik.de/europe/monaco-latest.osm.bz2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/cache@v3 with: path: | diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml index 3162c9ae9..785b2b89a 100644 --- a/.github/workflows/luacheck.yml +++ b/.github/workflows/luacheck.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install prerequisites run: | diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml index 9c91f8ce2..afec50300 100644 --- a/.github/workflows/test-install.yml +++ b/.github/workflows/test-install.yml @@ -28,7 +28,7 @@ jobs: OSMFILE: monaco-latest.osm.pbf steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Show installed PostgreSQL packages run: apt-cache search postgresql | sort From 616d0159a60cb28d5839d3085d781e71b57a44df Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sat, 2 Sep 2023 22:00:53 +0200 Subject: [PATCH 025/430] Make sure we are always testing with an empty grid in --append tests --- tests/bdd/flex/relation-changes.feature | 27 ++++++++++++++-------- tests/bdd/flex/way-add.feature | 18 ++++++++++----- tests/bdd/flex/way-change.feature | 15 ++++++++----- tests/bdd/flex/way-del.feature | 24 +++++++++++++------- tests/bdd/flex/way-relation-add.feature | 30 ++++++++++++++++--------- tests/bdd/flex/way-relation-del.feature | 15 ++++++++----- tests/bdd/steps/geometry_factory.py | 4 ++++ tests/bdd/steps/steps_osm_data.py | 5 +++++ 8 files changed, 95 insertions(+), 43 deletions(-) diff --git a/tests/bdd/flex/relation-changes.feature b/tests/bdd/flex/relation-changes.feature index 90e887a28..3ab06983d 100644 --- a/tests/bdd/flex/relation-changes.feature +++ b/tests/bdd/flex/relation-changes.feature @@ -32,7 +32,8 @@ Feature: Handling changes to relations | --slim | Then table osm2pgsql_test_relations has 0 rows - Given the OSM data + Given an empty grid + And the OSM data """ r30 v2 dV Ttype=multipolygon Mw20@,w21@ """ @@ -55,7 +56,8 @@ Feature: Handling changes to relations | --slim | Then table osm2pgsql_test_relations has 0 rows - Given the OSM data + Given an empty grid + And the OSM data """ w21 v2 dV Nn12,n13,n10 """ @@ -77,7 +79,8 @@ Feature: Handling changes to relations | --slim | Then table osm2pgsql_test_relations has 0 rows - Given the OSM data + Given an empty grid + And the OSM data """ n12 v2 dV x10.1 y10.1 """ @@ -100,7 +103,8 @@ Feature: Handling changes to relations | --slim | Then table osm2pgsql_test_relations has 0 rows - Given the OSM data + Given an empty grid + And the OSM data """ r30 v2 dV Ttype=multipolygon Mw20@,w21@ """ @@ -123,7 +127,8 @@ Feature: Handling changes to relations | --slim | Then table osm2pgsql_test_relations has 1 row - Given the OSM data + Given an empty grid + And the OSM data """ r30 v2 dV Mw20@,w21@ """ @@ -146,7 +151,8 @@ Feature: Handling changes to relations | --slim | Then table osm2pgsql_test_relations has 1 row - Given the OSM data + Given an empty grid + And the OSM data """ w21 v2 dV """ @@ -174,7 +180,8 @@ Feature: Handling changes to relations | --slim | Then table osm2pgsql_test_relations has 1 row - Given the OSM data + Given an empty grid + And the OSM data """ n12 v2 dV """ @@ -202,7 +209,8 @@ Feature: Handling changes to relations | --slim | Then table osm2pgsql_test_relations has 1 row - Given the OSM data + Given an empty grid + And the OSM data """ r30 v2 dV Ttype=multipolygon """ @@ -232,7 +240,8 @@ Feature: Handling changes to relations | area_id | tags->'natural' | tags->'landuse' | | -30 | wood | NULL | - Given the OSM data + Given an empty grid + And the OSM data """ r30 v2 dV Ttype=multipolygon,landuse=forest Mw20@,w21@ """ diff --git a/tests/bdd/flex/way-add.feature b/tests/bdd/flex/way-add.feature index 4b7ca86a1..bad8041ef 100644 --- a/tests/bdd/flex/way-add.feature +++ b/tests/bdd/flex/way-add.feature @@ -31,7 +31,8 @@ Feature: Adding ways to a flex database Scenario: way is not relevant - Given the OSM data + Given an empty grid + And the OSM data """ w10 v1 dV Tt=ag Nn10,n11 r30 v2 dV Tt=ag Mw10@,w11@,w12@mark,w13@,w14@mark @@ -52,7 +53,8 @@ Feature: Adding ways to a flex database Scenario: add to t1 - Given the OSM data + Given an empty grid + And the OSM data """ w10 v1 dV Tt1=yes Nn10,n11 r30 v2 dV Tt=ag Mw10@,w11@,w12@mark,w13@,w14@mark @@ -74,7 +76,8 @@ Feature: Adding ways to a flex database Scenario: add to t2 - Given the OSM data + Given an empty grid + And the OSM data """ w10 v1 dV Tt2=yes Nn10,n11 r30 v2 dV Tt=ag Mw10@mark,w11@,w12@mark,w13@,w14@mark @@ -96,7 +99,8 @@ Feature: Adding ways to a flex database Scenario: add to t1 and t2 - Given the OSM data + Given an empty grid + And the OSM data """ w10 v1 dV Tt1=yes,t2=yes Nn10,n11 r30 v2 dV Tt=ag Mw10@mark,w11@,w12@mark,w13@,w14@mark @@ -119,7 +123,8 @@ Feature: Adding ways to a flex database Scenario: add to tboth (only stage1) - Given the OSM data + Given an empty grid + And the OSM data """ w10 v1 dV Ttboth=yes Nn10,n11 r30 v2 dV Tt=ag Mw10@,w11@,w12@mark,w13@,w14@mark @@ -141,7 +146,8 @@ Feature: Adding ways to a flex database Scenario: add to tboth (stage1 and stage2) - Given the OSM data + Given an empty grid + And the OSM data """ w10 v1 dV Ttboth=yes Nn10,n11 r30 v2 dV Tt=ag Mw10@mark,w11@,w12@mark,w13@,w14@mark diff --git a/tests/bdd/flex/way-change.feature b/tests/bdd/flex/way-change.feature index 8c9855d3c..00499b268 100644 --- a/tests/bdd/flex/way-change.feature +++ b/tests/bdd/flex/way-change.feature @@ -31,7 +31,8 @@ Feature: Changing ways in a flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ """ @@ -83,7 +84,8 @@ Feature: Changing ways in a flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ """ @@ -136,7 +138,8 @@ Feature: Changing ways in a flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ """ @@ -193,7 +196,8 @@ Feature: Changing ways in a flex database | 13 | NULL | | 14 | {30} | - Given the OSM data + Given an empty grid + And the OSM data """ """ @@ -239,7 +243,8 @@ Feature: Changing ways in a flex database | 13 | NULL | | 14 | {30} | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dV Tt1=yes,t2=yes,tboth=yes Nn10,n11 """ diff --git a/tests/bdd/flex/way-del.feature b/tests/bdd/flex/way-del.feature index bc0d25d17..0e650e885 100644 --- a/tests/bdd/flex/way-del.feature +++ b/tests/bdd/flex/way-del.feature @@ -31,7 +31,8 @@ Feature: Deleting ways in a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dD """ @@ -73,7 +74,8 @@ Feature: Deleting ways in a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dD """ @@ -115,7 +117,8 @@ Feature: Deleting ways in a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dD """ @@ -156,7 +159,8 @@ Feature: Deleting ways in a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dD """ @@ -198,7 +202,8 @@ Feature: Deleting ways in a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dD """ @@ -240,7 +245,8 @@ Feature: Deleting ways in a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dD """ @@ -283,7 +289,8 @@ Feature: Deleting ways in a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dD """ @@ -325,7 +332,8 @@ Feature: Deleting ways in a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ w10 v2 dD """ diff --git a/tests/bdd/flex/way-relation-add.feature b/tests/bdd/flex/way-relation-add.feature index 3ec232b37..984f7862b 100644 --- a/tests/bdd/flex/way-relation-add.feature +++ b/tests/bdd/flex/way-relation-add.feature @@ -34,7 +34,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@mark,w11@,w12@,w13@,w14@,w15@ """ @@ -85,7 +86,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@mark,w11@,w12@,w13@,w14@,w15@ """ @@ -131,7 +133,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@mark,w11@,w12@,w13@,w14@,w15@ """ @@ -177,7 +180,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | NULL | | 14 | {30} | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@mark,w11@,w12@,w13@,w14@,w15@ """ @@ -227,7 +231,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | NULL | | 14 | {30} | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@mark,w11@,w12@,w13@,w14@,w15@ """ @@ -272,7 +277,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@,w11@,w12@,w13@,w14@,w15@ """ @@ -322,7 +328,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@mark,w11@,w12@,w13@,w14@,w15@ """ @@ -368,7 +375,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@,w11@,w12@,w13@,w14@,w15@ """ @@ -414,7 +422,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | NULL | | 14 | {30} | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@,w11@,w12@,w13@,w14@,w15@ """ @@ -459,7 +468,8 @@ Feature: Adding relations to a 2-stage flex database | 13 | NULL | | 14 | {30} | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dV Tt=ag Mw10@,w11@,w12@,w13@,w14@,w15@ """ diff --git a/tests/bdd/flex/way-relation-del.feature b/tests/bdd/flex/way-relation-del.feature index eb15b807d..1efc34ef0 100644 --- a/tests/bdd/flex/way-relation-del.feature +++ b/tests/bdd/flex/way-relation-del.feature @@ -33,7 +33,8 @@ Feature: Deleting relations in a stage-2 flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dD """ @@ -77,7 +78,8 @@ Feature: Deleting relations in a stage-2 flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ r32 v2 dD """ @@ -123,7 +125,8 @@ Feature: Deleting relations in a stage-2 flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ """ @@ -173,7 +176,8 @@ Feature: Deleting relations in a stage-2 flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ """ @@ -224,7 +228,8 @@ Feature: Deleting relations in a stage-2 flex database | 13 | | 14 | - Given the OSM data + Given an empty grid + And the OSM data """ """ diff --git a/tests/bdd/steps/geometry_factory.py b/tests/bdd/steps/geometry_factory.py index 54d24ac3c..be1c82bfa 100644 --- a/tests/bdd/steps/geometry_factory.py +++ b/tests/bdd/steps/geometry_factory.py @@ -64,6 +64,10 @@ def mk_wkt_points(self, geom): return ','.join([self.mk_wkt_point(x) for x in geom.split(',')]) + def remove_grid(self): + self.grid = {} + + def set_grid(self, lines, grid_step, origin_x, origin_y): """ Replace the grid with one from the given lines. """ diff --git a/tests/bdd/steps/steps_osm_data.py b/tests/bdd/steps/steps_osm_data.py index 26eff1a27..38f1fefb6 100644 --- a/tests/bdd/steps/steps_osm_data.py +++ b/tests/bdd/steps/steps_osm_data.py @@ -16,6 +16,11 @@ def osm_set_import_file(context, osm_file): context.import_file = context.test_data_dir / osm_file +@given("an empty grid") +def osm_define_node_grid(context): + context.geometry_factory.remove_grid() + + @given("the (?P[0-9.]+ )?grid(?: with origin (?P[0-9.-]+) (?P[0-9.-]+))?") def osm_define_node_grid(context, step, origin_x, origin_y): step = float(step.strip()) if step else 0.1 From 1199f90a0454eb4775f2eb51616c24135e136fea Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sat, 2 Sep 2023 22:01:22 +0200 Subject: [PATCH 026/430] Test that no maxzoom setting works on you'll get expire on zoom 0 --- tests/bdd/flex/expire.feature | 108 ++++++++++++++++++++++++++++++ tests/bdd/flex/lua-expire.feature | 23 +++++++ tests/data/test_expire.lua | 20 ++++++ 3 files changed, 151 insertions(+) create mode 100644 tests/bdd/flex/expire.feature create mode 100644 tests/data/test_expire.lua diff --git a/tests/bdd/flex/expire.feature b/tests/bdd/flex/expire.feature new file mode 100644 index 000000000..2230cf7a4 --- /dev/null +++ b/tests/bdd/flex/expire.feature @@ -0,0 +1,108 @@ +Feature: Changes on way with expire on zoom 0 + + Background: + Given the style file 'test_expire.lua' + + And the 0.1 grid + | 11 | 13 | + | 10 | 12 | + + And the OSM data + """ + w11 v1 dV Tt1=yes Nn12,n13 + """ + When running osm2pgsql flex with parameters + | --slim | + + Then table osm2pgsql_test_t1 contains exactly + | way_id | + | 11 | + Then table osm2pgsql_test_expire contains exactly + | zoom | x | y | + + + Scenario: way is not relevant + Given the OSM data + """ + w10 v1 dV Ta=b Nn10,n11 + """ + And an empty grid + + When running osm2pgsql flex with parameters + | --slim | -a | + + Then table osm2pgsql_test_t1 contains exactly + | way_id | + | 11 | + Then table osm2pgsql_test_expire contains exactly + | zoom | x | y | + + + Scenario: node is not relevant + Given the OSM data + """ + n1 v2 dV x1 y2 + """ + And an empty grid + + When running osm2pgsql flex with parameters + | --slim | -a | + + Then table osm2pgsql_test_t1 contains exactly + | way_id | + | 11 | + Then table osm2pgsql_test_expire contains exactly + | zoom | x | y | + + + Scenario: add to t1 + Given the OSM data + """ + w10 v1 dV Tt1=yes Nn10,n11 + """ + And an empty grid + + When running osm2pgsql flex with parameters + | --slim | -a | + + Then table osm2pgsql_test_t1 contains exactly + | way_id | + | 10 | + | 11 | + Then table osm2pgsql_test_expire contains exactly + | zoom | x | y | + | 0 | 0 | 0 | + + + Scenario: change in t1 + Given the OSM data + """ + w11 v2 dV Ta=b Nn10,n11 + """ + And an empty grid + + When running osm2pgsql flex with parameters + | --slim | -a | + + Then table osm2pgsql_test_t1 contains exactly + | way_id | + Then table osm2pgsql_test_expire contains exactly + | zoom | x | y | + | 0 | 0 | 0 | + + + Scenario: remove from t1 + Given the OSM data + """ + w11 v2 dD + """ + And an empty grid + + When running osm2pgsql flex with parameters + | --slim | -a | + + Then table osm2pgsql_test_t1 contains exactly + | way_id | + Then table osm2pgsql_test_expire contains exactly + | zoom | x | y | + | 0 | 0 | 0 | diff --git a/tests/bdd/flex/lua-expire.feature b/tests/bdd/flex/lua-expire.feature index 6998c8684..d24fc0bfd 100644 --- a/tests/bdd/flex/lua-expire.feature +++ b/tests/bdd/flex/lua-expire.feature @@ -238,3 +238,26 @@ Feature: Expire configuration in Lua file Then table nodes has 1562 rows And table tiles has 0 rows + Scenario: Expire into table without maxzoom means maxzoom 0 + Given the input file 'liechtenstein-2013-08-03.osm.pbf' + And the lua style + """ + local eo = osm2pgsql.define_expire_output({ + table = 'tiles', + }) + local t = osm2pgsql.define_node_table('nodes', { + { column = 'geom', + type = 'point', + expire = { + { output = eo } + }} + }) + + function osm2pgsql.process_node(object) + t:insert({ geom = object:as_point() }) + end + """ + When running osm2pgsql flex + Then table nodes has 1562 rows + And table tiles has 0 rows + diff --git a/tests/data/test_expire.lua b/tests/data/test_expire.lua new file mode 100644 index 000000000..bd949e743 --- /dev/null +++ b/tests/data/test_expire.lua @@ -0,0 +1,20 @@ + +-- No maxzoom sets it to 0 +local eo = osm2pgsql.define_expire_output({ + table = 'osm2pgsql_test_expire', +}) + +local the_table = osm2pgsql.define_way_table('osm2pgsql_test_t1', { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'linestring', expire = {{ output = eo }} }, +}) + +function osm2pgsql.process_way(object) + if object.tags.t1 then + the_table:insert{ + tags = object.tags, + geom = object:as_linestring() + } + end +end + From 92625b5c0007c6f3e40d60c7e4a193d1f8487c33 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Wed, 6 Sep 2023 11:47:09 +0200 Subject: [PATCH 027/430] Generalization: Wrap upload of data in transaction So we don't use so many transaction ids. --- src/gen/gen-tile-builtup.cpp | 2 ++ src/gen/gen-tile-raster.cpp | 2 ++ src/gen/gen-tile-vector.cpp | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/gen/gen-tile-builtup.cpp b/src/gen/gen-tile-builtup.cpp index 6992b030c..6cae4f2b4 100644 --- a/src/gen/gen-tile-builtup.cpp +++ b/src/gen/gen-tile-builtup.cpp @@ -181,6 +181,7 @@ static void draw_from_db(double margin, canvas_list_t *canvas_list, void gen_tile_builtup_t::process(tile_t const &tile) { + connection().exec("BEGIN"); delete_existing(tile); canvas_list_t canvas_list; @@ -262,6 +263,7 @@ void gen_tile_builtup_t::process(tile_t const &tile) connection().exec_prepared("insert_geoms", wkb, tile.x(), tile.y()); } } + connection().exec("COMMIT"); timer(m_timer_write).stop(); log_gen("Inserted {} generalized polygons", geometries.size()); } diff --git a/src/gen/gen-tile-raster.cpp b/src/gen/gen-tile-raster.cpp index 6ac7ba0db..9ac0873ab 100644 --- a/src/gen/gen-tile-raster.cpp +++ b/src/gen/gen-tile-raster.cpp @@ -174,6 +174,7 @@ static void draw_from_db(double margin, unsigned int image_extent, void gen_tile_raster_union_t::process(tile_t const &tile) { + connection().exec("BEGIN"); delete_existing(tile); canvas_list_t canvas_list; @@ -244,6 +245,7 @@ void gen_tile_raster_union_t::process(tile_t const &tile) timer(m_timer_write).stop(); log_gen("Inserted {} generalized polygons", geometries.size()); } + connection().exec("COMMIT"); } void gen_tile_raster_union_t::post() diff --git a/src/gen/gen-tile-vector.cpp b/src/gen/gen-tile-vector.cpp index 89cefccff..cdc270a6b 100644 --- a/src/gen/gen-tile-vector.cpp +++ b/src/gen/gen-tile-vector.cpp @@ -83,12 +83,14 @@ PREPARE gen_geoms (int, int, int) AS void gen_tile_vector_union_t::process(tile_t const &tile) { + connection().exec("BEGIN"); delete_existing(tile); log_gen("Generalize..."); timer(m_timer_simplify).start(); auto const result = connection().exec_prepared("gen_geoms", tile.zoom(), tile.x(), tile.y()); + connection().exec("COMMIT"); timer(m_timer_simplify).stop(); log_gen("Inserted {} generalized polygons", result.affected_rows()); } From ff6d92efb13c2ce7b6ae655017206313ab0fad61 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sat, 9 Sep 2023 15:33:23 +0200 Subject: [PATCH 028/430] Generalizer: Log tile being processed also in multithreaded mode Not only in singlethreaded more. --- src/gen/osm2pgsql-gen.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index e892dd16c..eb60a4c06 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -262,7 +262,10 @@ void run_tile_gen(std::string const &conninfo, gen_base_t *master_generalizer, queue->pop_back(); } - generalizer->process({zoom, p.first, p.second}); + tile_t tile{zoom, p.first, p.second}; + log_debug("Processing tile {}/{}/{}...", tile.zoom(), tile.x(), + tile.y()); + generalizer->process(tile); } log_debug("Shutting down generalizer thread."); } From 723b64d4257d9a8463d4b59bee5b5e7edc2e1492 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sat, 9 Sep 2023 15:46:45 +0200 Subject: [PATCH 029/430] Generalizer: Simplify tracer code, use local instead of member vars --- src/gen/tracer.cpp | 14 ++++++++------ src/gen/tracer.hpp | 2 -- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/gen/tracer.cpp b/src/gen/tracer.cpp index 62cd37905..e0bcddcad 100644 --- a/src/gen/tracer.cpp +++ b/src/gen/tracer.cpp @@ -26,18 +26,22 @@ tracer_t::trace(canvas_t const &canvas, tile_t const &tile, double min_area) { prepare(canvas); - m_state.reset(potrace_trace(m_param.get(), &m_bitmap)); - if (!m_state || m_state->status != POTRACE_STATUS_OK) { + potrace_bitmap_t bitmap{int(canvas.size()), int(canvas.size()), + int(canvas.size() / bits_per_word), m_bits.data()}; + + std::unique_ptr state{ + potrace_trace(m_param.get(), &bitmap)}; + + if (!state || state->status != POTRACE_STATUS_OK) { throw std::runtime_error{"potrace failed"}; } - return build_geometries(tile, m_state->plist, min_area); + return build_geometries(tile, state->plist, min_area); } void tracer_t::reset() { m_bits.clear(); - m_state.reset(); m_num_points = 0; } @@ -70,8 +74,6 @@ void tracer_t::prepare(canvas_t const &canvas) noexcept m_bits.push_back(w); } - - m_bitmap = {int(size), int(size), int(size / bits_per_word), m_bits.data()}; } std::vector diff --git a/src/gen/tracer.hpp b/src/gen/tracer.hpp index 6026070e6..47fc587f6 100644 --- a/src/gen/tracer.hpp +++ b/src/gen/tracer.hpp @@ -64,9 +64,7 @@ class tracer_t double min_area) noexcept; std::vector m_bits; - potrace_bitmap_t m_bitmap{}; std::unique_ptr m_param; - std::unique_ptr m_state; std::size_t m_extent; std::size_t m_buffer; std::size_t m_num_points = 0; From b15de24fe32b6944a300579cfe63d2be4e368a27 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sun, 10 Sep 2023 09:32:18 +0200 Subject: [PATCH 030/430] Generalizer bugfix: Set delete_existing earlier In append mode we need to delete existing tiles. We need to set this parameter earlier, so the generalizer is initialized correctly. --- src/gen/osm2pgsql-gen.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index e892dd16c..817564afd 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -317,6 +317,10 @@ class genproc_t params.set("schema", m_dbschema); } + if (m_append) { + params.set("delete_existing", true); + } + write_to_debug_log(params, "Params (config):"); log_debug("Connecting to database..."); @@ -329,10 +333,6 @@ class genproc_t log_info("Running generalizer '{}' ({})...", generalizer->name(), generalizer->strategy()); - if (m_append) { - params.set("delete_existing", true); - } - write_to_debug_log(params, "Params (after initialization):"); util::timer_t timer_gen; From b5fedee6cae4612697b54ca914021d72c3d07e81 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Fri, 8 Sep 2023 20:35:53 +0200 Subject: [PATCH 031/430] Check values found in expire tables (must be number in valid range) --- src/gen/osm2pgsql-gen.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index a751fd126..6a3db8867 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -201,16 +201,28 @@ static tile_extent get_extent_from_db(pg_conn_t const &db_connection, } static std::vector> -get_tiles_from_table(pg_conn_t const &connection, std::string const &table) +get_tiles_from_table(pg_conn_t const &connection, std::string const &table, + uint32_t zoom) { - std::vector> tiles; + auto const result = connection.exec( + R"(SELECT x, y FROM "{}" WHERE zoom = {})", table, zoom); - auto const result = connection.exec(R"(SELECT x, y FROM "{}")", table); + std::vector> tiles; + tiles.reserve(result.num_tuples()); + uint32_t const max = 1UL << zoom; for (int n = 0; n < result.num_tuples(); ++n) { char *end = nullptr; auto const x = std::strtoul(result.get_value(n, 0), &end, 10); + if (*end != '\0' || x >= max) { + log_error("Ignoring invalid x value in expire table '{}'", table); + continue; + } auto const y = std::strtoul(result.get_value(n, 1), &end, 10); + if (*end != '\0' || y >= max) { + log_error("Ignoring invalid y value in expire table '{}'", table); + continue; + } tiles.emplace_back(x, y); } @@ -497,7 +509,7 @@ class genproc_t auto const table = params.get_string("expire_list"); log_debug("Running generalizer for expire list from table '{}'...", table); - tile_list = get_tiles_from_table(db_connection, table); + tile_list = get_tiles_from_table(db_connection, table, zoom); log_debug("Truncating table '{}'...", table); db_connection.exec("TRUNCATE {}", table); } else { From fb0d5d57858b4560ee8fb53a9216a867de5107de Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 18 Sep 2023 10:56:16 +0200 Subject: [PATCH 032/430] Add first/last timestamps to expire tables This adds two timestamp (with timezone) columns to all expire tables called "first" and "last". When an entry is added, both are set to the current (transaction) timestamp. When an entry already exists a new insert will result in the "last" timestamp being updated. Having these two timestamps allows various expire/updating strategies, for instance: * update oldest entry * update entry that didn't change for the longest time * update older entries but only if there are no recent changes, which indicates that there might be more changes coming For backwards compatibility the code detects which table format is used and falls back to the old behaviour if the timestamp columns aren't there. --- src/expire-output.cpp | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/expire-output.cpp b/src/expire-output.cpp index ef26f5135..6a03aac87 100644 --- a/src/expire-output.cpp +++ b/src/expire-output.cpp @@ -59,10 +59,22 @@ expire_output_t::output_tiles_to_table(quadkey_list_t const &tiles_at_maxzoom, pg_conn_t connection{conninfo}; - connection.exec("PREPARE insert_tiles(int4, int4, int4) AS" - " INSERT INTO {} (zoom, x, y) VALUES ($1, $2, $3)" - " ON CONFLICT DO NOTHING", - qn); + auto const result = connection.exec("SELECT * FROM {} LIMIT 1", qn); + + if (result.num_fields() == 3) { + // old format with fields: zoom, x, y + connection.exec("PREPARE insert_tiles(int4, int4, int4) AS" + " INSERT INTO {} (zoom, x, y) VALUES ($1, $2, $3)" + " ON CONFLICT DO NOTHING", + qn); + } else { + // new format with fields: zoom, x, y, first, last + connection.exec("PREPARE insert_tiles(int4, int4, int4) AS" + " INSERT INTO {} (zoom, x, y) VALUES ($1, $2, $3)" + " ON CONFLICT (zoom, x, y)" + " DO UPDATE SET last = CURRENT_TIMESTAMP(0)", + qn); + } auto const count = for_each_tile( tiles_at_maxzoom, m_minzoom, m_maxzoom, [&](tile_t const &tile) { @@ -76,10 +88,13 @@ expire_output_t::output_tiles_to_table(quadkey_list_t const &tiles_at_maxzoom, void expire_output_t::create_output_table(pg_conn_t const &connection) const { auto const qn = qualified_name(m_schema, m_table); - connection.exec("CREATE TABLE IF NOT EXISTS {} (" - " zoom int4 NOT NULL," - " x int4 NOT NULL," - " y int4 NOT NULL," - " PRIMARY KEY (zoom, x, y))", - qn); + connection.exec( + "CREATE TABLE IF NOT EXISTS {} (" + " zoom int4 NOT NULL," + " x int4 NOT NULL," + " y int4 NOT NULL," + " first timestamp with time zone DEFAULT CURRENT_TIMESTAMP(0)," + " last timestamp with time zone DEFAULT CURRENT_TIMESTAMP(0)," + " PRIMARY KEY (zoom, x, y))", + qn); } From 33bd0a464bd830bc429baf1e72fce6665dffd07d Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Tue, 19 Sep 2023 10:24:34 +0200 Subject: [PATCH 033/430] Github actions: Test with newer PostgreSQL versions (up to 16) --- .github/workflows/ci.yml | 48 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0adf68fe..6a3a51426 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,7 +121,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu20-pg12-gcc10-jit: + ubuntu20-pg13-gcc10-jit: runs-on: ubuntu-20.04 env: @@ -129,7 +129,7 @@ jobs: CXX: g++-10 LUA_VERSION: 5.3 LUAJIT_OPTION: ON - POSTGRESQL_VERSION: 12 + POSTGRESQL_VERSION: 13 POSTGIS_VERSION: 2.5 BUILD_TYPE: Debug PSYCOPG: 2 @@ -140,7 +140,7 @@ jobs: - uses: ./.github/actions/build-and-test - ubuntu20-pg13-clang10-jit: + ubuntu20-pg15-clang10-jit: runs-on: ubuntu-20.04 env: @@ -148,7 +148,7 @@ jobs: CXX: clang++-10 LUA_VERSION: 5.3 LUAJIT_OPTION: ON - POSTGRESQL_VERSION: 13 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 BUILD_TYPE: Debug PSYCOPG: 2 @@ -158,7 +158,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu20-pg13-clang10-proj6: + ubuntu20-pg15-clang10-proj6: runs-on: ubuntu-20.04 env: @@ -166,7 +166,7 @@ jobs: CXX: clang++-10 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 13 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 USE_PROJ_LIB: 6 BUILD_TYPE: Debug @@ -177,7 +177,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu20-pg13-clang10-noproj: + ubuntu20-pg15-clang10-noproj: runs-on: ubuntu-20.04 env: @@ -185,7 +185,7 @@ jobs: CXX: clang++-10 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 13 + POSTGRESQL_VERSION: 15 POSTGIS_VERSION: 3 USE_PROJ_LIB: off BUILD_TYPE: Debug @@ -196,7 +196,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu20-pg15-clang10: + ubuntu20-pg16-clang10: runs-on: ubuntu-20.04 env: @@ -204,7 +204,7 @@ jobs: CXX: clang++-10 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 15 + POSTGRESQL_VERSION: 16 POSTGIS_VERSION: 3 USE_PROJ_LIB: 6 BUILD_TYPE: Debug @@ -249,7 +249,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg15-clang14-jit: + ubuntu22-pg16-clang14-jit: runs-on: ubuntu-22.04 env: @@ -257,7 +257,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.4 LUAJIT_OPTION: ON - POSTGRESQL_VERSION: 15 + POSTGRESQL_VERSION: 16 POSTGIS_VERSION: 3 BUILD_TYPE: Debug PSYCOPG: 2 @@ -267,7 +267,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg15-clang14-proj6: + ubuntu22-pg16-clang14-proj6: runs-on: ubuntu-22.04 env: @@ -275,7 +275,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.4 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 15 + POSTGRESQL_VERSION: 16 POSTGIS_VERSION: 3 USE_PROJ_LIB: 6 BUILD_TYPE: Debug @@ -286,7 +286,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg15-clang14-noproj: + ubuntu22-pg16-clang14-noproj: runs-on: ubuntu-22.04 env: @@ -294,7 +294,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 15 + POSTGRESQL_VERSION: 16 POSTGIS_VERSION: 3 USE_PROJ_LIB: off BUILD_TYPE: Debug @@ -324,7 +324,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg15-gcc12-release: + ubuntu22-pg16-gcc12-release: runs-on: ubuntu-22.04 env: @@ -333,7 +333,7 @@ jobs: EXTRA_FLAGS: -Wno-stringop-overread LUA_VERSION: 5.4 LUAJIT_OPTION: ON - POSTGRESQL_VERSION: 15 + POSTGRESQL_VERSION: 16 POSTGIS_VERSION: 3 BUILD_TYPE: Release PSYCOPG: 2 @@ -343,14 +343,14 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg15-gcc12-release-nolua: + ubuntu22-pg16-gcc12-release-nolua: runs-on: ubuntu-22.04 env: CC: gcc-12 CXX: g++-12 EXTRA_FLAGS: -Wno-stringop-overread - POSTGRESQL_VERSION: 15 + POSTGRESQL_VERSION: 16 POSTGIS_VERSION: 3 BUILD_TYPE: Release PSYCOPG: 2 @@ -360,7 +360,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg15-clang14-cpp20: + ubuntu22-pg16-clang14-cpp20: runs-on: ubuntu-22.04 env: @@ -368,7 +368,7 @@ jobs: CXX: clang++-14 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 15 + POSTGRESQL_VERSION: 16 POSTGIS_VERSION: 3 CPP_VERSION: 20 BUILD_TYPE: Debug @@ -379,7 +379,7 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg15-gcc12-cpp20: + ubuntu22-pg16-gcc12-cpp20: runs-on: ubuntu-22.04 env: @@ -387,7 +387,7 @@ jobs: CXX: g++-12 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF - POSTGRESQL_VERSION: 15 + POSTGRESQL_VERSION: 16 POSTGIS_VERSION: 3 CPP_VERSION: 20 BUILD_TYPE: Debug From 203889c31c44400aabe5abf7b735e7dc273b51ba Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Fri, 22 Sep 2023 09:43:24 +0200 Subject: [PATCH 034/430] Avoid copy of vector in get_tiles_from_table() function Use out parameter with ptr instead. --- src/gen/osm2pgsql-gen.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index 6a3db8867..e8351c5af 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -200,15 +200,15 @@ static tile_extent get_extent_from_db(pg_conn_t const &db_connection, return get_extent_from_db(db_connection, schema, table, geom_column, zoom); } -static std::vector> +static void get_tiles_from_table(pg_conn_t const &connection, std::string const &table, - uint32_t zoom) + uint32_t zoom, + std::vector> *tiles) { auto const result = connection.exec( R"(SELECT x, y FROM "{}" WHERE zoom = {})", table, zoom); - std::vector> tiles; - tiles.reserve(result.num_tuples()); + tiles->reserve(result.num_tuples()); uint32_t const max = 1UL << zoom; for (int n = 0; n < result.num_tuples(); ++n) { @@ -223,10 +223,8 @@ get_tiles_from_table(pg_conn_t const &connection, std::string const &table, log_error("Ignoring invalid y value in expire table '{}'", table); continue; } - tiles.emplace_back(x, y); + tiles->emplace_back(x, y); } - - return tiles; } class tile_processor_t @@ -509,7 +507,7 @@ class genproc_t auto const table = params.get_string("expire_list"); log_debug("Running generalizer for expire list from table '{}'...", table); - tile_list = get_tiles_from_table(db_connection, table, zoom); + get_tiles_from_table(db_connection, table, zoom, &tile_list); log_debug("Truncating table '{}'...", table); db_connection.exec("TRUNCATE {}", table); } else { From 845995c8430b2f14dd3f6dae77e69a36bddf526a Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 4 Sep 2023 11:34:05 +0200 Subject: [PATCH 035/430] Generalization: Switch from using CImg to OpenCV library. Processing is an order of magnitude faster. --- .../actions/ubuntu-prerequisites/action.yml | 4 +- .github/actions/win-install/action.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/test-install.yml | 4 +- CMakeLists.txt | 11 ++- README.md | 9 +-- src/gen/canvas.cpp | 72 ++++++++++++------- src/gen/canvas.hpp | 43 +++++------ src/gen/osm2pgsql-gen.cpp | 1 - 9 files changed, 83 insertions(+), 65 deletions(-) diff --git a/.github/actions/ubuntu-prerequisites/action.yml b/.github/actions/ubuntu-prerequisites/action.yml index c2b3248d2..a7e9ef42c 100644 --- a/.github/actions/ubuntu-prerequisites/action.yml +++ b/.github/actions/ubuntu-prerequisites/action.yml @@ -16,11 +16,13 @@ runs: - name: Install software run: | sudo apt-get install -yq --no-install-suggests --no-install-recommends \ - cimg-dev \ libboost-filesystem-dev \ libboost-system-dev \ libbz2-dev \ libexpat1-dev \ + libopencv-core-dev \ + libopencv-imgcodecs-dev \ + libopencv-imgproc-dev \ libpotrace-dev \ libpq-dev \ libproj-dev \ diff --git a/.github/actions/win-install/action.yml b/.github/actions/win-install/action.yml index 47eaf79cc..ff558df73 100644 --- a/.github/actions/win-install/action.yml +++ b/.github/actions/win-install/action.yml @@ -12,11 +12,11 @@ runs: boost-property-tree:x64-windows \ boost-system:x64-windows \ bzip2:x64-windows \ - cimg:x64-windows \ expat:x64-windows \ libpq:x64-windows \ lua:x64-windows \ nlohmann-json:x64-windows \ + opencv:x64-windows \ proj4:x64-windows \ zlib:x64-windows shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a3a51426..95d2b6001 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Install prerequisites run: | - brew install lua boost postgis pandoc cimg potrace nlohmann-json + brew install boost lua nlohmann-json opencv pandoc postgis potrace pip3 install psycopg2 behave osmium pg_ctl -D /usr/local/var/postgres init pg_ctl -D /usr/local/var/postgres start diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml index afec50300..2dd9acadd 100644 --- a/.github/workflows/test-install.yml +++ b/.github/workflows/test-install.yml @@ -38,13 +38,15 @@ jobs: sudo apt-get purge -yq postgresql* sudo apt-get update -qq sudo apt-get install -yq --no-install-suggests --no-install-recommends \ - cimg-dev \ libboost-filesystem-dev \ libboost-system-dev \ libbz2-dev \ libexpat1-dev \ liblua${LUA_VERSION}-dev \ libluajit-5.1-dev \ + libopencv-core-dev \ + libopencv-imgcodecs-dev \ + libopencv-imgproc-dev \ libpotrace-dev \ libpq-dev \ libproj-dev \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c1f38de3..c715ce1d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -211,7 +211,7 @@ include_directories(SYSTEM ${NLOHMANN_INCLUDE_DIR}) find_path(POTRACE_INCLUDE_DIR potracelib.h) find_library(POTRACE_LIBRARY NAMES potrace) -find_path(CIMG_INCLUDE_DIR CImg.h) +find_package(OpenCV OPTIONAL_COMPONENTS core imgcodecs imgproc) ############### Libraries are found now ######################## @@ -284,13 +284,12 @@ add_subdirectory(src) add_executable(osm2pgsql src/osm2pgsql.cpp) target_link_libraries(osm2pgsql osm2pgsql_lib ${LIBS}) -if (${POTRACE_LIBRARY} STREQUAL "POTRACE_LIBRARY-NOTFOUND" OR ${CIMG_INCLUDE_DIR} STREQUAL "CIMG_INCLUDE_DIR-NOTFOUND") - message(STATUS "Did not find cimg and/or potrace library. Not building osm2pgsql-gen.") +if (${POTRACE_LIBRARY} STREQUAL "POTRACE_LIBRARY-NOTFOUND" OR NOT OPENCV_CORE_FOUND) + message(STATUS "Did not find opencv and/or potrace library. Not building osm2pgsql-gen.") else() if (WITH_LUA) - message(STATUS "Found cimg and potrace library. Building osm2pgsql-gen.") + message(STATUS "Found opencv and potrace library. Building osm2pgsql-gen.") set(BUILD_GEN 1) - include_directories(SYSTEM ${CIMG_INCLUDE_DIR}) include_directories(SYSTEM ${POTRACE_INCLUDE_DIR}) add_executable(osm2pgsql-gen src/gen/osm2pgsql-gen.cpp src/gen/canvas.cpp @@ -305,7 +304,7 @@ else() src/gen/params.cpp src/gen/raster.cpp src/gen/tracer.cpp) - target_link_libraries(osm2pgsql-gen osm2pgsql_lib ${LIBS} ${POTRACE_LIBRARY}) + target_link_libraries(osm2pgsql-gen osm2pgsql_lib ${LIBS} ${POTRACE_LIBRARY} ${OpenCV_LIBS}) else() message(STATUS "No Lua. Not building osm2pgsql-gen.") endif() diff --git a/README.md b/README.md index 06e9d68fc..71936bcb3 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Required libraries are * [Boost libraries](https://www.boost.org/), including geometry, system and filesystem * [nlohmann/json](https://json.nlohmann.me/) -* [CImg](https://cimg.eu/) (Optional, for generalization only) +* [OpenCV](https://opencv.org/) (Optional, for generalization only) * [potrace](https://potrace.sourceforge.net/) (Optional, for generalization only) * [PostgreSQL](https://www.postgresql.org/) client libraries * [Lua](https://www.lua.org/) (Optional, used for Lua tag transforms @@ -83,7 +83,8 @@ On a Debian or Ubuntu system, this can be done with: ```sh sudo apt-get install make cmake g++ libboost-dev libboost-system-dev \ - libboost-filesystem-dev libexpat1-dev zlib1g-dev libpotrace-dev cimg-dev \ + libboost-filesystem-dev libexpat1-dev zlib1g-dev libpotrace-dev \ + libopencv-core-dev libopencv-imgcodecs-dev libopencv-imgproc-dev \ libbz2-dev libpq-dev libproj-dev lua5.3 liblua5.3-dev pandoc \ nlohmann-json3-dev pyosmium ``` @@ -92,7 +93,7 @@ On a Fedora system, use ```sh sudo dnf install cmake make gcc-c++ boost-devel expat-devel zlib-devel \ - potrace-devel cimg-devel json-devel python3-osmium \ + potrace-devel opencv-devel json-devel python3-osmium \ bzip2-devel postgresql-devel proj-devel proj-epsg lua-devel pandoc ``` @@ -101,7 +102,7 @@ dependencies with: ```sh sudo yum install cmake make gcc-c++ boost-devel expat-devel zlib-devel \ - potrace-devel cimg-devel json-devel python3-osmium \ + potrace-devel opencv-devel json-devel python3-osmium \ bzip2-devel postgresql-devel proj-devel proj-epsg lua-devel pandoc ``` diff --git a/src/gen/canvas.cpp b/src/gen/canvas.cpp index faa402e54..ecb0ef156 100644 --- a/src/gen/canvas.cpp +++ b/src/gen/canvas.cpp @@ -8,40 +8,56 @@ */ #include "canvas.hpp" + #include "raster.hpp" -cimg_library::CImg canvas_t::create_pointlist(geom::point_list_t const &pl, - tile_t const &tile) const +#include +#include + +void canvas_t::open_close(unsigned int buffer_size) { - cimg_library::CImg points{static_cast(pl.size()), 2}; + auto const kernel1 = cv::getStructuringElement( + cv::MORPH_RECT, + cv::Size(static_cast(buffer_size), static_cast(buffer_size))); + auto const kernel2 = cv::getStructuringElement( + cv::MORPH_RECT, cv::Size(static_cast(buffer_size * 2), + static_cast(buffer_size * 2))); + + cv::erode(m_rast, m_rast, kernel1); + cv::dilate(m_rast, m_rast, kernel2); + cv::erode(m_rast, m_rast, kernel1); +} + +void canvas_t::create_pointlist(std::vector *out, + geom::point_list_t const &pl, + tile_t const &tile) const +{ + out->reserve(pl.size()); - int n = 0; for (auto const point : pl) { auto const tp = tile.to_tile_coords(point, m_extent); - points(n, 0) = static_cast(static_cast(m_buffer) + tp.x()); - points(n, 1) = - static_cast(static_cast(m_buffer + m_extent) - tp.y()); - ++n; + auto const x = static_cast(m_buffer) + tp.x(); + auto const y = static_cast(m_buffer + m_extent) - tp.y(); + out->emplace_back(x, y); } - - return points; } std::size_t canvas_t::draw_polygon(geom::polygon_t const &polygon, tile_t const &tile) { - if (polygon.inners().empty()) { - m_rast.draw_polygon(create_pointlist(polygon.outer(), tile), &White); - return polygon.outer().size(); - } - std::size_t num_points = polygon.outer().size(); - m_temp.draw_polygon(create_pointlist(polygon.outer(), tile), &White); + + std::vector> poly_data; + poly_data.resize(polygon.inners().size() + 1); + + create_pointlist(poly_data.data(), polygon.outer(), tile); + std::size_t n = 1; for (auto const &inner : polygon.inners()) { num_points += inner.size(); - m_temp.draw_polygon(create_pointlist(inner, tile), &Black); + create_pointlist(&poly_data[n], inner, tile); + n++; } - m_rast |= m_temp; + cv::fillPoly(m_rast, poly_data, cv::Scalar{255}); return num_points; } @@ -49,7 +65,9 @@ std::size_t canvas_t::draw_polygon(geom::polygon_t const &polygon, std::size_t canvas_t::draw_linestring(geom::linestring_t const &linestring, tile_t const &tile) { - m_rast.draw_line(create_pointlist(linestring, tile), &White); + std::vector line_data; + create_pointlist(&line_data, linestring, tile); + cv::polylines(m_rast, line_data, false, cv::Scalar{255}); return linestring.size(); } @@ -81,13 +99,13 @@ std::size_t canvas_t::draw(geom::geometry_t const &geometry, tile_t const &tile) void canvas_t::save(std::string const &filename) const { - m_rast.save(filename.c_str()); + cv::imwrite(filename, m_rast); } std::string canvas_t::to_wkb(tile_t const &tile, double margin) const { std::string wkb; - wkb.reserve(61 + 2 + m_rast.size()); + wkb.reserve(61 + 2 + size() * size()); // header wkb_raster_header header{}; @@ -106,18 +124,24 @@ std::string canvas_t::to_wkb(tile_t const &tile, double margin) const add_raster_band(&wkb, band); // rasterdata - wkb.append(reinterpret_cast(m_rast.data()), m_rast.size()); + wkb.append(reinterpret_cast(begin()), + reinterpret_cast(end())); - assert(wkb.size() == 61 + 2 + m_rast.size()); + assert(wkb.size() == 61 + 2 + size() * size()); return wkb; } -void canvas_t::merge(canvas_t const &other) { m_rast |= other.m_rast; } +void canvas_t::merge(canvas_t const &other) +{ + cv::bitwise_or(m_rast, other.m_rast, m_rast); +} std::string to_hex(std::string const &in) { std::string result; + result.reserve(in.size() * 2); + char const *const lookup_hex = "0123456789ABCDEF"; for (const auto c : in) { diff --git a/src/gen/canvas.hpp b/src/gen/canvas.hpp index bc439b947..6a6eba639 100644 --- a/src/gen/canvas.hpp +++ b/src/gen/canvas.hpp @@ -13,48 +13,42 @@ #include "geom.hpp" #include "tile.hpp" -#define cimg_display 0 // NOLINT(cppcoreguidelines-macro-usage) -#include "CImg.h" +#include #include /** - * This class wraps the image class from the CImg library. + * This class wraps the image class from the OpenCV library. */ class canvas_t { public: - static void info() { cimg_library::cimg::info(); } - /** * Create a new image canvas. It will be quadratic and have the width and * height extent + 2*buffer. */ canvas_t(std::size_t extent, std::size_t buffer) - : m_extent(extent), - m_buffer(buffer), m_rast{size(), size(), 1, 1, 0}, m_temp{size(), size(), - 1, 1, 0} - {} + : m_extent(extent), m_buffer(buffer), m_rast{static_cast(size()), + static_cast(size()), + CV_8UC1, cv::Scalar::all(0)} + { + } unsigned int size() const noexcept { return static_cast(m_extent + 2 * m_buffer); } - unsigned char const *begin() const noexcept { return m_rast.begin(); } - unsigned char const *end() const noexcept { return m_rast.end(); } - - std::size_t draw(geom::geometry_t const &geometry, tile_t const &tile); + unsigned char const *begin() const noexcept { return m_rast.data; } - unsigned char operator()(int x, int y) const noexcept + unsigned char const *end() const noexcept { - return m_rast(x, y, 0, 0); + return m_rast.data + (static_cast(size() * size())); } - void open_close(unsigned int buffer_size) - { - m_rast.dilate(buffer_size).erode(buffer_size * 2).dilate(buffer_size); - } + std::size_t draw(geom::geometry_t const &geometry, tile_t const &tile); + + void open_close(unsigned int buffer_size); void save(std::string const &filename) const; @@ -63,13 +57,11 @@ class canvas_t void merge(canvas_t const &other); private: - constexpr static unsigned char const Black = 0; - constexpr static unsigned char const White = 255; - - using image_type = cimg_library::CImg; + using image_type = cv::Mat; - cimg_library::CImg create_pointlist(geom::point_list_t const &pl, - tile_t const &tile) const; + void create_pointlist(std::vector *out, + geom::point_list_t const &pl, + tile_t const &tile) const; std::size_t draw_polygon(geom::polygon_t const &polygon, tile_t const &tile); @@ -80,7 +72,6 @@ class canvas_t std::size_t m_extent; std::size_t m_buffer; image_type m_rast; - image_type m_temp; }; // class canvas_t std::string to_hex(std::string const &in); diff --git a/src/gen/osm2pgsql-gen.cpp b/src/gen/osm2pgsql-gen.cpp index 6a3db8867..ca727522e 100644 --- a/src/gen/osm2pgsql-gen.cpp +++ b/src/gen/osm2pgsql-gen.cpp @@ -712,7 +712,6 @@ int main(int argc, char *argv[]) break; case 'V': // --version log_info("osm2pgsql-gen version {}", get_osm2pgsql_version()); - canvas_t::info(); return 0; case 201: // --log-sql get_logger().enable_sql(); From e1c1630a9ad49dcae1586107714be589891d9bf7 Mon Sep 17 00:00:00 2001 From: Paul Norman Date: Mon, 2 Oct 2023 19:22:56 -0700 Subject: [PATCH 036/430] Require libopencv-dev on Debian systems This package contains the CMake files needed to build osm2pgsql. --- .github/actions/ubuntu-prerequisites/action.yml | 4 +--- README.md | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/actions/ubuntu-prerequisites/action.yml b/.github/actions/ubuntu-prerequisites/action.yml index a7e9ef42c..2299044c8 100644 --- a/.github/actions/ubuntu-prerequisites/action.yml +++ b/.github/actions/ubuntu-prerequisites/action.yml @@ -20,9 +20,7 @@ runs: libboost-system-dev \ libbz2-dev \ libexpat1-dev \ - libopencv-core-dev \ - libopencv-imgcodecs-dev \ - libopencv-imgproc-dev \ + libopencv-dev \ libpotrace-dev \ libpq-dev \ libproj-dev \ diff --git a/README.md b/README.md index 71936bcb3..94012eeda 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,8 @@ On a Debian or Ubuntu system, this can be done with: ```sh sudo apt-get install make cmake g++ libboost-dev libboost-system-dev \ libboost-filesystem-dev libexpat1-dev zlib1g-dev libpotrace-dev \ - libopencv-core-dev libopencv-imgcodecs-dev libopencv-imgproc-dev \ - libbz2-dev libpq-dev libproj-dev lua5.3 liblua5.3-dev pandoc \ - nlohmann-json3-dev pyosmium + libopencv-dev libbz2-dev libpq-dev libproj-dev lua5.3 liblua5.3-dev \ + pandoc nlohmann-json3-dev pyosmium ``` On a Fedora system, use From 397e286ad688f2aa1a96ed3eae2d299dd4c0206a Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 21 Aug 2023 10:22:51 +0200 Subject: [PATCH 037/430] Refactor: Modernize copy functions using std::string_view --- src/middle-pgsql.cpp | 2 +- src/pgsql.cpp | 15 +++++++-------- src/pgsql.hpp | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/middle-pgsql.cpp b/src/middle-pgsql.cpp index 45ef7fdf8..6daed404c 100644 --- a/src/middle-pgsql.cpp +++ b/src/middle-pgsql.cpp @@ -64,7 +64,7 @@ static void send_id_list(pg_conn_t const &db_connection, } auto const sql = fmt::format("COPY {} FROM STDIN", table); - db_connection.copy_start(sql.c_str()); + db_connection.copy_start(sql); db_connection.copy_send(data, table); db_connection.copy_end(table); } diff --git a/src/pgsql.cpp b/src/pgsql.cpp index 2fb16d700..2a6808a42 100644 --- a/src/pgsql.cpp +++ b/src/pgsql.cpp @@ -91,25 +91,24 @@ pg_result_t pg_conn_t::exec(std::string const &sql) const return exec(sql.c_str()); } -void pg_conn_t::copy_start(char const *sql) const +void pg_conn_t::copy_start(std::string_view sql) const { assert(m_conn); log_sql("(C{}) {}", m_connection_id, sql); - pg_result_t const res{PQexec(m_conn.get(), sql)}; + pg_result_t const res{PQexec(m_conn.get(), sql.data())}; if (res.status() != PGRES_COPY_IN) { throw fmt_error("Database error on COPY: {}", error_msg()); } } -void pg_conn_t::copy_send(std::string const &data, - std::string const &context) const +void pg_conn_t::copy_send(std::string_view data, std::string_view context) const { assert(m_conn); log_sql_data("(C{}) Copy data to '{}':\n{}", m_connection_id, context, data); - int const r = PQputCopyData(m_conn.get(), data.c_str(), (int)data.size()); + int const r = PQputCopyData(m_conn.get(), data.data(), (int)data.size()); switch (r) { case 0: // need to wait for write ready @@ -127,14 +126,14 @@ void pg_conn_t::copy_send(std::string const &data, if (data.size() < 1100) { log_error("Data: {}", data); } else { - log_error("Data: {}\n...\n{}", std::string(data, 0, 500), - std::string(data, data.size() - 500)); + log_error("Data: {}\n...\n{}", data.substr(0, 500), + data.substr(data.size() - 500, 500)); } throw std::runtime_error{"COPYing data to Postgresql."}; } -void pg_conn_t::copy_end(std::string const &context) const +void pg_conn_t::copy_end(std::string_view context) const { assert(m_conn); diff --git a/src/pgsql.hpp b/src/pgsql.hpp index 99b27996c..f472eaf4d 100644 --- a/src/pgsql.hpp +++ b/src/pgsql.hpp @@ -215,9 +215,9 @@ class pg_conn_t */ void set_config(char const *setting, char const *value) const; - void copy_start(char const *sql) const; - void copy_send(std::string const &data, std::string const &context) const; - void copy_end(std::string const &context) const; + void copy_start(std::string_view sql) const; + void copy_send(std::string_view data, std::string_view context) const; + void copy_end(std::string_view context) const; /// Return the latest generated error message on this connection. char const *error_msg() const noexcept; From 90ac2c1475a97842e6cc09ff1ea0a33b8366d5ca Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 21 Aug 2023 10:43:04 +0200 Subject: [PATCH 038/430] Refactor: Modernize code, use nested namespace definition --- tests/common-cleanup.hpp | 6 ++---- tests/common-pg.hpp | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/common-cleanup.hpp b/tests/common-cleanup.hpp index 7da2bc2de..8e5cee6b1 100644 --- a/tests/common-cleanup.hpp +++ b/tests/common-cleanup.hpp @@ -16,8 +16,7 @@ #include -namespace testing { -namespace cleanup { +namespace testing::cleanup { /** * RAII structure to remove a file upon destruction. @@ -56,7 +55,6 @@ class file_t std::string m_filename; }; -} // namespace cleanup -} // namespace testing +} // namespace testing::cleanup #endif // OSM2PGSQL_TESTS_COMMON_CLEANUP_HPP diff --git a/tests/common-pg.hpp b/tests/common-pg.hpp index 4069a81de..01a7e1255 100644 --- a/tests/common-pg.hpp +++ b/tests/common-pg.hpp @@ -30,9 +30,8 @@ #include #endif -namespace testing { /// Helper classes for postgres connections -namespace pg { +namespace testing::pg { class conn_t : public pg_conn_t { @@ -153,7 +152,6 @@ class tempdb_t std::string m_db_name; }; -} // namespace pg -} // namespace testing +} // namespace testing::pg #endif // OSM2PGSQL_TESTS_COMMON_PG_HPP From 368fbccaa9a46ea4b8180b54478e17fdb2cf6dcc Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 21 Aug 2023 10:41:32 +0200 Subject: [PATCH 039/430] Refactor: Modernize get_count and require_has_table Use string_view parameters --- tests/common-pg.hpp | 6 ++++-- tests/test-properties.cpp | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/common-pg.hpp b/tests/common-pg.hpp index 4069a81de..a917ee9a3 100644 --- a/tests/common-pg.hpp +++ b/tests/common-pg.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "format.hpp" #include "options.hpp" @@ -76,7 +77,8 @@ class conn_t : public pg_conn_t return res; } - int get_count(char const *table_name, std::string const &where = "") const + int get_count(std::string_view table_name, + std::string_view where = "") const { auto const query = fmt::format("SELECT count(*) FROM {} {} {}", table_name, @@ -85,7 +87,7 @@ class conn_t : public pg_conn_t return result_as_int(query); } - void require_has_table(char const *table_name) const + void require_has_table(std::string_view table_name) const { auto const where = fmt::format("oid = '{}'::regclass", table_name); diff --git a/tests/test-properties.cpp b/tests/test-properties.cpp index 42f0a28c8..3edc8384a 100644 --- a/tests/test-properties.cpp +++ b/tests/test-properties.cpp @@ -67,14 +67,14 @@ TEST_CASE("Store and retrieve properties (with database)") std::string const full_table_name = (schema.empty() ? "" : schema + ".") + "osm2pgsql_properties"; - REQUIRE(conn.get_count(full_table_name.c_str()) == 4); - REQUIRE(conn.get_count(full_table_name.c_str(), + REQUIRE(conn.get_count(full_table_name) == 4); + REQUIRE(conn.get_count(full_table_name, "property='foo' AND value='bar'") == 1); - REQUIRE(conn.get_count(full_table_name.c_str(), + REQUIRE(conn.get_count(full_table_name, "property='empty' AND value=''") == 1); - REQUIRE(conn.get_count(full_table_name.c_str(), + REQUIRE(conn.get_count(full_table_name, "property='number' AND value='123'") == 1); - REQUIRE(conn.get_count(full_table_name.c_str(), + REQUIRE(conn.get_count(full_table_name, "property='decide' AND value='true'") == 1); properties_t properties{db.conninfo(), schema}; From c7fbcb6b77e16ea9403aa08462e495926727ac4c Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Mon, 30 Oct 2023 08:48:00 +0100 Subject: [PATCH 040/430] Github action: Use clang 15 for C++20 test --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95d2b6001..4adbcbf60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -360,12 +360,12 @@ jobs: - uses: ./.github/actions/ubuntu-prerequisites - uses: ./.github/actions/build-and-test - ubuntu22-pg16-clang14-cpp20: + ubuntu22-pg16-clang15-cpp20: runs-on: ubuntu-22.04 env: - CC: clang-14 - CXX: clang++-14 + CC: clang-15 + CXX: clang++-15 LUA_VERSION: 5.3 LUAJIT_OPTION: OFF POSTGRESQL_VERSION: 16 From 5c4dcc1aaa2f6578ddd89c95768e0cd745dce4a0 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Tue, 24 Oct 2023 13:59:55 +0200 Subject: [PATCH 041/430] Use tags = NULL in middle tables if object doesn't have any tags This doesn't make much of a difference for the ways and rels table, but if we store all nodes in the database, it does make a huge difference, because most nodes don't have any tags. For a current planet, disk usage for the nodes table goes from 476 GB down to 409 GB saving 67 GB or nearly 15%. Additionally it makes use of that table simpler. If you want to do any queries on tags, you need an index on the tags column on the nodes/ways/rels tables like this: CREATE INDEX ON planet_osm_ways USING gin (tags); But that is wasteful, because of the empty tags. We probably want to generate them as CREATE INDEX ON planet_osm_ways USING gin (tags) WHERE tags != '{}'::jsonb; But now all queries on those tables have to include that extra condition so that the query planner will use the index. SELECT * FROM planet_osm_ways WHERE tags ? 'highway' AND tags != '{}'::jsonb; If we use NULLs, the index can be created as: CREATE INDEX ON planet_osm_ways USING gin (tags) WHERE tags IS NOT NULL; And now the query becomes simpler, because the NOT NULL is automatically taken into account by the query planner: SELECT * FROM planet_osm_ways WHERE tags ? 'highway'; Note that this is an incompatible change to the new format middle tables, but they are still marked as experimental, so we can do this. --- src/middle-pgsql.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/middle-pgsql.cpp b/src/middle-pgsql.cpp index 6daed404c..e7ca279a1 100644 --- a/src/middle-pgsql.cpp +++ b/src/middle-pgsql.cpp @@ -323,6 +323,10 @@ template void pgsql_parse_json_tags(char const *string, osmium::memory::Buffer *buffer, T *obuilder) { + if (*string == '\0') { // NULL + return; + } + auto const tags = nlohmann::json::parse(string); if (!tags.is_object()) { throw std::runtime_error{"Database format for tags invalid."}; @@ -613,6 +617,10 @@ void middle_pgsql_t::copy_attributes(osmium::OSMObject const &obj) void middle_pgsql_t::copy_tags(osmium::OSMObject const &obj) { if (m_store_options.db_format == 2) { + if (obj.tags().empty()) { + m_db_copy.add_null_column(); + return; + } json_writer_t writer; tags_to_json(obj.tags(), &writer); m_db_copy.add_column(writer.json()); @@ -1464,7 +1472,7 @@ static table_sql sql_for_nodes_format2(middle_pgsql_options const &options) " lat int4 NOT NULL," " lon int4 NOT NULL," "{attribute_columns_definition}" - " tags jsonb NOT NULL" + " tags jsonb" ") {data_tablespace}"; sql.prepare_queries = { @@ -1530,7 +1538,7 @@ static table_sql sql_for_ways_format2(middle_pgsql_options const &options) " id int8 PRIMARY KEY {using_tablespace}," "{attribute_columns_definition}" " nodes int8[] NOT NULL," - " tags jsonb NOT NULL" + " tags jsonb" ") {data_tablespace}"; sql.prepare_queries = {"PREPARE get_way(int8) AS" @@ -1601,7 +1609,7 @@ static table_sql sql_for_relations_format2() " id int8 PRIMARY KEY {using_tablespace}," "{attribute_columns_definition}" " members jsonb NOT NULL," - " tags jsonb NOT NULL" + " tags jsonb" ") {data_tablespace}"; sql.prepare_queries = {"PREPARE get_rel(int8) AS" From 5b25afede97417477c325134cb059b685f1914d1 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sat, 28 Oct 2023 14:53:41 +0200 Subject: [PATCH 042/430] Allow NULL values in new middle format members list This makes osm2pgsql a bit more future proof by allowing the list of members (which is encoded as JSON in the new middle format) to be empty, i.e. to contain NULL. We currently don't write empty member lists as NULL but as an empty JSON list, but if we change this in the future, older versions of osm2pgsql will be able to read this correctly. --- src/middle-pgsql.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/middle-pgsql.cpp b/src/middle-pgsql.cpp index e7ca279a1..a673472a5 100644 --- a/src/middle-pgsql.cpp +++ b/src/middle-pgsql.cpp @@ -443,6 +443,10 @@ template void pgsql_parse_json_members(char const *string, osmium::memory::Buffer *buffer, T *obuilder) { + if (*string == '\0') { // NULL + return; + } + osmium::builder::RelationMemberListBuilder builder{*buffer, obuilder}; member_list_json_builder parser{&builder}; nlohmann::json::sax_parse(string, &parser); From 2f0ec97632ad58b5e9c5d389f2ce1d0b129cf575 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Sun, 29 Oct 2023 09:57:21 +0100 Subject: [PATCH 043/430] Update included fmt lib to 10.1.0 --- contrib/fmt/LICENSE.rst | 2 +- contrib/fmt/README.contrib | 2 +- contrib/fmt/README.rst | 67 +- contrib/fmt/include/fmt/args.h | 2 +- contrib/fmt/include/fmt/chrono.h | 793 ++++++----- contrib/fmt/include/fmt/color.h | 55 +- contrib/fmt/include/fmt/compile.h | 131 +- contrib/fmt/include/fmt/core.h | 1883 ++++++++++---------------- contrib/fmt/include/fmt/format-inl.h | 243 ++-- contrib/fmt/include/fmt/format.h | 1848 ++++++++++++++----------- contrib/fmt/include/fmt/locale.h | 2 - contrib/fmt/include/fmt/os.h | 115 +- contrib/fmt/include/fmt/ostream.h | 72 +- contrib/fmt/include/fmt/posix.h | 2 - contrib/fmt/include/fmt/printf.h | 375 ++--- contrib/fmt/include/fmt/ranges.h | 303 +++-- contrib/fmt/include/fmt/std.h | 343 ++++- contrib/fmt/include/fmt/xchar.h | 95 +- 18 files changed, 3244 insertions(+), 3089 deletions(-) delete mode 100644 contrib/fmt/include/fmt/locale.h delete mode 100644 contrib/fmt/include/fmt/posix.h diff --git a/contrib/fmt/LICENSE.rst b/contrib/fmt/LICENSE.rst index f0ec3db4d..1cd1ef926 100644 --- a/contrib/fmt/LICENSE.rst +++ b/contrib/fmt/LICENSE.rst @@ -1,4 +1,4 @@ -Copyright (c) 2012 - present, Victor Zverovich +Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/contrib/fmt/README.contrib b/contrib/fmt/README.contrib index ef4f7977e..326599199 100644 --- a/contrib/fmt/README.contrib +++ b/contrib/fmt/README.contrib @@ -1,2 +1,2 @@ Source: https://github.com/fmtlib/fmt -Revision: v9.1.0 +Revision: v10.1.0 diff --git a/contrib/fmt/README.rst b/contrib/fmt/README.rst index cc6d7c41a..f51fefab7 100644 --- a/contrib/fmt/README.rst +++ b/contrib/fmt/README.rst @@ -47,7 +47,9 @@ Features * `Format string syntax `_ similar to Python's `format `_ * Fast IEEE 754 floating-point formatter with correct rounding, shortness and - round-trip guarantees + round-trip guarantees using the `Dragonbox `_ + algorithm +* Portable Unicode support * Safe `printf implementation `_ including the POSIX extension for positional arguments @@ -64,7 +66,7 @@ Features `_ and is `continuously fuzzed `_ -* Safety: the library is fully type safe, errors in format strings can be +* Safety: the library is fully type-safe, errors in format strings can be reported at compile time, automatic memory management prevents buffer overflow errors * Ease of use: small self-contained code base, no external dependencies, @@ -74,7 +76,7 @@ Features consistent output across platforms and support for older compilers * Clean warning-free codebase even on high warning levels such as ``-Wall -Wextra -pedantic`` -* Locale-independence by default +* Locale independence by default * Optional header-only configuration enabled with the ``FMT_HEADER_ONLY`` macro See the `documentation `_ for more details. @@ -191,24 +193,24 @@ Speed tests ================= ============= =========== Library Method Run Time, s ================= ============= =========== -libc printf 1.04 -libc++ std::ostream 3.05 -{fmt} 6.1.1 fmt::print 0.75 -Boost Format 1.67 boost::format 7.24 -Folly Format folly::format 2.23 +libc printf 0.91 +libc++ std::ostream 2.49 +{fmt} 9.1 fmt::print 0.74 +Boost Format 1.80 boost::format 6.26 +Folly Format folly::format 1.87 ================= ============= =========== -{fmt} is the fastest of the benchmarked methods, ~35% faster than ``printf``. +{fmt} is the fastest of the benchmarked methods, ~20% faster than ``printf``. The above results were generated by building ``tinyformat_test.cpp`` on macOS -10.14.6 with ``clang++ -O3 -DNDEBUG -DSPEED_TEST -DHAVE_FORMAT``, and taking the +12.6.1 with ``clang++ -O3 -DNDEBUG -DSPEED_TEST -DHAVE_FORMAT``, and taking the best of three runs. In the test, the format string ``"%0.10f:%04d:%+g:%s:%p:%c:%%\n"`` or equivalent is filled 2,000,000 times with output sent to ``/dev/null``; for further details refer to the `source `_. {fmt} is up to 20-30x faster than ``std::ostringstream`` and ``sprintf`` on -floating-point formatting (`dtoa-benchmark `_) +IEEE754 ``float`` and ``double`` formatting (`dtoa-benchmark `_) and faster than `double-conversion `_ and `ryu `_: @@ -224,7 +226,7 @@ The script `bloat-test.py from `format-benchmark `_ tests compile time and code bloat for nontrivial projects. It generates 100 translation units and uses ``printf()`` or its alternative -five times in each to simulate a medium sized project. The resulting +five times in each to simulate a medium-sized project. The resulting executable size and compile time (Apple LLVM version 8.1.0 (clang-802.0.42), macOS Sierra, best of three) is shown in the following tables. @@ -245,7 +247,7 @@ As you can see, {fmt} has 60% less overhead in terms of resulting binary code size compared to iostreams and comes pretty close to ``printf``. Boost Format and Folly Format have the largest overheads. -``printf+string`` is the same as ``printf`` but with extra ```` +``printf+string`` is the same as ``printf`` but with an extra ```` include to measure the overhead of the latter. **Non-optimized build** @@ -261,14 +263,14 @@ Boost Format 54.1 365 303 Folly Format 79.9 445 430 ============= =============== ==================== ================== -``libc``, ``lib(std)c++`` and ``libfmt`` are all linked as shared libraries to +``libc``, ``lib(std)c++``, and ``libfmt`` are all linked as shared libraries to compare formatting function overhead only. Boost Format is a header-only library so it doesn't provide any linkage options. Running the tests ~~~~~~~~~~~~~~~~~ -Please refer to `Building the library`__ for the instructions on how to build +Please refer to `Building the library`__ for instructions on how to build the library and run the unit tests. __ https://fmt.dev/latest/usage.html#building-the-library @@ -293,9 +295,12 @@ or the bloat test:: Migrating code -------------- -`clang-tidy-fmt `_ provides clang -tidy checks for converting occurrences of ``printf`` and ``fprintf`` to -``fmt::print``. +`clang-tidy `_ v17 (not yet +released) provides the `modernize-use-std-print +`_ +check that is capable of converting occurrences of ``printf`` and +``fprintf`` to ``fmt::print`` if configured to do so. (By default it +converts to ``std::print``.) Projects using this library --------------------------- @@ -303,8 +308,6 @@ Projects using this library * `0 A.D. `_: a free, open-source, cross-platform real-time strategy game -* `2GIS `_: free business listings with a city map - * `AMPL/MP `_: an open-source library for mathematical programming @@ -322,8 +325,10 @@ Projects using this library * `ccache `_: a compiler cache -* `ClickHouse `_: analytical database +* `ClickHouse `_: an analytical database management system + +* `Contour `_: a modern terminal emulator * `CUAUV `_: Cornell University's autonomous underwater vehicle @@ -360,6 +365,10 @@ Projects using this library * `Knuth `_: high-performance Bitcoin full-node +* `libunicode `_: a modern C++17 Unicode library + +* `MariaDB `_: relational database management system + * `Microsoft Verona `_: research programming language for concurrent ownership @@ -389,7 +398,7 @@ Projects using this library proxy * `redpanda `_: a 10x faster Kafka® replacement - for mission critical systems written in C++ + for mission-critical systems written in C++ * `rpclib `_: a modern C++ msgpack-RPC server and client library @@ -413,6 +422,9 @@ Projects using this library * `TrinityCore `_: open-source MMORPG framework +* `🐙 userver framework `_: open-source asynchronous + framework with a rich set of abstractions and database drivers + * `Windows Terminal `_: the new Windows terminal @@ -470,7 +482,7 @@ error handling is awkward. Boost Format ~~~~~~~~~~~~ -This is a very powerful library which supports both ``printf``-like format +This is a very powerful library that supports both ``printf``-like format strings and positional arguments. Its main drawback is performance. According to various benchmarks, it is much slower than other methods considered here. Boost Format also has excessive build times and severe code bloat issues (see @@ -479,7 +491,7 @@ Format also has excessive build times and severe code bloat issues (see FastFormat ~~~~~~~~~~ -This is an interesting library which is fast, safe and has positional arguments. +This is an interesting library that is fast, safe, and has positional arguments. However, it has significant limitations, citing its author: Three features that have no hope of being accommodated within the @@ -495,7 +507,7 @@ restrictive for using it in some projects. Boost Spirit.Karma ~~~~~~~~~~~~~~~~~~ -This is not really a formatting library but I decided to include it here for +This is not a formatting library but I decided to include it here for completeness. As iostreams, it suffers from the problem of mixing verbatim text with arguments. The library is pretty fast, but slower on integer formatting than ``fmt::format_to`` with format string compilation on Karma's own benchmark, @@ -514,7 +526,7 @@ Documentation License The `Format String Syntax `_ section in the documentation is based on the one from Python `string module documentation `_. -For this reason the documentation is distributed under the Python Software +For this reason, the documentation is distributed under the Python Software Foundation license available in `doc/python-license.txt `_. It only applies if you distribute the documentation of {fmt}. @@ -523,8 +535,7 @@ Maintainers ----------- The {fmt} library is maintained by Victor Zverovich (`vitaut -`_) and Jonathan Müller (`foonathan -`_) with contributions from many other people. +`_) with contributions from many other people. See `Contributors `_ and `Releases `_ for some of the names. Let us know if your contribution is not listed or mentioned incorrectly and diff --git a/contrib/fmt/include/fmt/args.h b/contrib/fmt/include/fmt/args.h index a3966d140..2d684e7cc 100644 --- a/contrib/fmt/include/fmt/args.h +++ b/contrib/fmt/include/fmt/args.h @@ -1,4 +1,4 @@ -// Formatting library for C++ - dynamic format arguments +// Formatting library for C++ - dynamic argument lists // // Copyright (c) 2012 - present, Victor Zverovich // All rights reserved. diff --git a/contrib/fmt/include/fmt/chrono.h b/contrib/fmt/include/fmt/chrono.h index b112f76e9..ff3e1445b 100644 --- a/contrib/fmt/include/fmt/chrono.h +++ b/contrib/fmt/include/fmt/chrono.h @@ -22,6 +22,24 @@ FMT_BEGIN_NAMESPACE +// Check if std::chrono::local_t is available. +#ifndef FMT_USE_LOCAL_TIME +# ifdef __cpp_lib_chrono +# define FMT_USE_LOCAL_TIME (__cpp_lib_chrono >= 201907L) +# else +# define FMT_USE_LOCAL_TIME 0 +# endif +#endif + +// Check if std::chrono::utc_timestamp is available. +#ifndef FMT_USE_UTC_TIME +# ifdef __cpp_lib_chrono +# define FMT_USE_UTC_TIME (__cpp_lib_chrono >= 201907L) +# else +# define FMT_USE_UTC_TIME 0 +# endif +#endif + // Enable tzset. #ifndef FMT_USE_TZSET // UWP doesn't provide _tzset. @@ -203,7 +221,8 @@ To safe_duration_cast(std::chrono::duration from, } const auto min1 = (std::numeric_limits::min)() / Factor::num; - if (!std::is_unsigned::value && count < min1) { + if (detail::const_check(!std::is_unsigned::value) && + count < min1) { ec = 1; return {}; } @@ -358,37 +377,11 @@ auto write_encoded_tm_str(OutputIt out, string_view in, const std::locale& loc) unit_t unit; write_codecvt(unit, in, loc); // In UTF-8 is used one to four one-byte code units. - auto&& buf = basic_memory_buffer(); - for (code_unit* p = unit.buf; p != unit.end; ++p) { - uint32_t c = static_cast(*p); - if (sizeof(code_unit) == 2 && c >= 0xd800 && c <= 0xdfff) { - // surrogate pair - ++p; - if (p == unit.end || (c & 0xfc00) != 0xd800 || - (*p & 0xfc00) != 0xdc00) { - FMT_THROW(format_error("failed to format time")); - } - c = (c << 10) + static_cast(*p) - 0x35fdc00; - } - if (c < 0x80) { - buf.push_back(static_cast(c)); - } else if (c < 0x800) { - buf.push_back(static_cast(0xc0 | (c >> 6))); - buf.push_back(static_cast(0x80 | (c & 0x3f))); - } else if ((c >= 0x800 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xffff)) { - buf.push_back(static_cast(0xe0 | (c >> 12))); - buf.push_back(static_cast(0x80 | ((c & 0xfff) >> 6))); - buf.push_back(static_cast(0x80 | (c & 0x3f))); - } else if (c >= 0x10000 && c <= 0x10ffff) { - buf.push_back(static_cast(0xf0 | (c >> 18))); - buf.push_back(static_cast(0x80 | ((c & 0x3ffff) >> 12))); - buf.push_back(static_cast(0x80 | ((c & 0xfff) >> 6))); - buf.push_back(static_cast(0x80 | (c & 0x3f))); - } else { - FMT_THROW(format_error("failed to format time")); - } - } - return copy_str(buf.data(), buf.data() + buf.size(), out); + auto u = + to_utf8>(); + if (!u.convert({unit.buf, to_unsigned(unit.end - unit.buf)})) + FMT_THROW(format_error("failed to format time")); + return copy_str(u.c_str(), u.c_str() + u.size(), out); } return copy_str(in.data(), in.data() + in.size(), out); } @@ -427,7 +420,7 @@ auto write(OutputIt out, const std::tm& time, const std::locale& loc, char format, char modifier = 0) -> OutputIt { auto&& buf = get_buffer(out); do_write(buf, time, loc, format, modifier); - return buf.out(); + return get_iterator(buf, out); } template time_point) { - return localtime(std::chrono::system_clock::to_time_t(time_point)); +#if FMT_USE_LOCAL_TIME +template +inline auto localtime(std::chrono::local_time time) -> std::tm { + return localtime(std::chrono::system_clock::to_time_t( + std::chrono::current_zone()->to_sys(time))); } +#endif /** Converts given time since epoch as ``std::time_t`` value into calendar time, @@ -523,7 +519,7 @@ inline std::tm gmtime(std::time_t time) { } #endif }; - dispatcher gt(time); + auto gt = dispatcher(time); // Too big time values may be unsupported. if (!gt.run()) FMT_THROW(format_error("time_t value out of range")); return gt.tm_; @@ -534,7 +530,7 @@ inline std::tm gmtime( return gmtime(std::chrono::system_clock::to_time_t(time_point)); } -FMT_BEGIN_DETAIL_NAMESPACE +namespace detail { // Writes two-digit numbers a, b and c separated by sep to buf. // The method by Pavel Novikov based on @@ -599,12 +595,39 @@ enum class numeric_system { alternative }; +// Glibc extensions for formatting numeric values. +enum class pad_type { + unspecified, + // Do not pad a numeric result string. + none, + // Pad a numeric result string with zeros even if the conversion specifier + // character uses space-padding by default. + zero, + // Pad a numeric result string with spaces. + space, +}; + +template +auto write_padding(OutputIt out, pad_type pad, int width) -> OutputIt { + if (pad == pad_type::none) return out; + return std::fill_n(out, width, pad == pad_type::space ? ' ' : '0'); +} + +template +auto write_padding(OutputIt out, pad_type pad) -> OutputIt { + if (pad != pad_type::none) *out++ = pad == pad_type::space ? ' ' : '0'; + return out; +} + // Parses a put_time-like format string and invokes handler actions. template FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, const Char* end, Handler&& handler) { + if (begin == end || *begin == '}') return begin; + if (*begin != '%') FMT_THROW(format_error("invalid format")); auto ptr = begin; + pad_type pad = pad_type::unspecified; while (ptr != end) { auto c = *ptr; if (c == '}') break; @@ -615,6 +638,22 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, if (begin != ptr) handler.on_text(begin, ptr); ++ptr; // consume '%' if (ptr == end) FMT_THROW(format_error("invalid format")); + c = *ptr; + switch (c) { + case '_': + pad = pad_type::space; + ++ptr; + break; + case '-': + pad = pad_type::none; + ++ptr; + break; + case '0': + pad = pad_type::zero; + ++ptr; + break; + } + if (ptr == end) FMT_THROW(format_error("invalid format")); c = *ptr++; switch (c) { case '%': @@ -691,16 +730,16 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, break; // Hour, minute, second: case 'H': - handler.on_24_hour(numeric_system::standard); + handler.on_24_hour(numeric_system::standard, pad); break; case 'I': - handler.on_12_hour(numeric_system::standard); + handler.on_12_hour(numeric_system::standard, pad); break; case 'M': - handler.on_minute(numeric_system::standard); + handler.on_minute(numeric_system::standard, pad); break; case 'S': - handler.on_second(numeric_system::standard); + handler.on_second(numeric_system::standard, pad); break; // Other: case 'c': @@ -737,7 +776,7 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, handler.on_duration_unit(); break; case 'z': - handler.on_utc_offset(); + handler.on_utc_offset(numeric_system::standard); break; case 'Z': handler.on_tz_name(); @@ -765,6 +804,9 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, case 'X': handler.on_loc_time(numeric_system::alternative); break; + case 'z': + handler.on_utc_offset(numeric_system::alternative); + break; default: FMT_THROW(format_error("invalid format")); } @@ -802,16 +844,19 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, handler.on_dec1_weekday(numeric_system::alternative); break; case 'H': - handler.on_24_hour(numeric_system::alternative); + handler.on_24_hour(numeric_system::alternative, pad); break; case 'I': - handler.on_12_hour(numeric_system::alternative); + handler.on_12_hour(numeric_system::alternative, pad); break; case 'M': - handler.on_minute(numeric_system::alternative); + handler.on_minute(numeric_system::alternative, pad); break; case 'S': - handler.on_second(numeric_system::alternative); + handler.on_second(numeric_system::alternative, pad); + break; + case 'z': + handler.on_utc_offset(numeric_system::alternative); break; default: FMT_THROW(format_error("invalid format")); @@ -864,7 +909,7 @@ template struct null_chrono_spec_handler { FMT_CONSTEXPR void on_am_pm() { unsupported(); } FMT_CONSTEXPR void on_duration_value() { unsupported(); } FMT_CONSTEXPR void on_duration_unit() { unsupported(); } - FMT_CONSTEXPR void on_utc_offset() { unsupported(); } + FMT_CONSTEXPR void on_utc_offset(numeric_system) { unsupported(); } FMT_CONSTEXPR void on_tz_name() { unsupported(); } }; @@ -892,10 +937,10 @@ struct tm_format_checker : null_chrono_spec_handler { FMT_CONSTEXPR void on_day_of_year() {} FMT_CONSTEXPR void on_day_of_month(numeric_system) {} FMT_CONSTEXPR void on_day_of_month_space(numeric_system) {} - FMT_CONSTEXPR void on_24_hour(numeric_system) {} - FMT_CONSTEXPR void on_12_hour(numeric_system) {} - FMT_CONSTEXPR void on_minute(numeric_system) {} - FMT_CONSTEXPR void on_second(numeric_system) {} + FMT_CONSTEXPR void on_24_hour(numeric_system, pad_type) {} + FMT_CONSTEXPR void on_12_hour(numeric_system, pad_type) {} + FMT_CONSTEXPR void on_minute(numeric_system, pad_type) {} + FMT_CONSTEXPR void on_second(numeric_system, pad_type) {} FMT_CONSTEXPR void on_datetime(numeric_system) {} FMT_CONSTEXPR void on_loc_date(numeric_system) {} FMT_CONSTEXPR void on_loc_time(numeric_system) {} @@ -905,7 +950,7 @@ struct tm_format_checker : null_chrono_spec_handler { FMT_CONSTEXPR void on_24_hour_time() {} FMT_CONSTEXPR void on_iso_time() {} FMT_CONSTEXPR void on_am_pm() {} - FMT_CONSTEXPR void on_utc_offset() {} + FMT_CONSTEXPR void on_utc_offset(numeric_system) {} FMT_CONSTEXPR void on_tz_name() {} }; @@ -957,13 +1002,130 @@ inline void tzset_once() { } #endif -template class tm_writer { +// Converts value to Int and checks that it's in the range [0, upper). +template ::value)> +inline Int to_nonnegative_int(T value, Int upper) { + FMT_ASSERT(std::is_unsigned::value || + (value >= 0 && to_unsigned(value) <= to_unsigned(upper)), + "invalid value"); + (void)upper; + return static_cast(value); +} +template ::value)> +inline Int to_nonnegative_int(T value, Int upper) { + if (value < 0 || value > static_cast(upper)) + FMT_THROW(format_error("invalid value")); + return static_cast(value); +} + +constexpr long long pow10(std::uint32_t n) { + return n == 0 ? 1 : 10 * pow10(n - 1); +} + +// Counts the number of fractional digits in the range [0, 18] according to the +// C++20 spec. If more than 18 fractional digits are required then returns 6 for +// microseconds precision. +template () / 10)> +struct count_fractional_digits { + static constexpr int value = + Num % Den == 0 ? N : count_fractional_digits::value; +}; + +// Base case that doesn't instantiate any more templates +// in order to avoid overflow. +template +struct count_fractional_digits { + static constexpr int value = (Num % Den == 0) ? N : 6; +}; + +// Format subseconds which are given as an integer type with an appropriate +// number of digits. +template +void write_fractional_seconds(OutputIt& out, Duration d, int precision = -1) { + constexpr auto num_fractional_digits = + count_fractional_digits::value; + + using subsecond_precision = std::chrono::duration< + typename std::common_type::type, + std::ratio<1, detail::pow10(num_fractional_digits)>>; + + const auto fractional = + d - std::chrono::duration_cast(d); + const auto subseconds = + std::chrono::treat_as_floating_point< + typename subsecond_precision::rep>::value + ? fractional.count() + : std::chrono::duration_cast(fractional).count(); + auto n = static_cast>(subseconds); + const int num_digits = detail::count_digits(n); + + int leading_zeroes = (std::max)(0, num_fractional_digits - num_digits); + if (precision < 0) { + FMT_ASSERT(!std::is_floating_point::value, ""); + if (std::ratio_less::value) { + *out++ = '.'; + out = std::fill_n(out, leading_zeroes, '0'); + out = format_decimal(out, n, num_digits).end; + } + } else { + *out++ = '.'; + leading_zeroes = (std::min)(leading_zeroes, precision); + out = std::fill_n(out, leading_zeroes, '0'); + int remaining = precision - leading_zeroes; + if (remaining != 0 && remaining < num_digits) { + n /= to_unsigned(detail::pow10(to_unsigned(num_digits - remaining))); + out = format_decimal(out, n, remaining).end; + return; + } + out = format_decimal(out, n, num_digits).end; + remaining -= num_digits; + out = std::fill_n(out, remaining, '0'); + } +} + +// Format subseconds which are given as a floating point type with an +// appropriate number of digits. We cannot pass the Duration here, as we +// explicitly need to pass the Rep value in the chrono_formatter. +template +void write_floating_seconds(memory_buffer& buf, Duration duration, + int num_fractional_digits = -1) { + using rep = typename Duration::rep; + FMT_ASSERT(std::is_floating_point::value, ""); + + auto val = duration.count(); + + if (num_fractional_digits < 0) { + // For `std::round` with fallback to `round`: + // On some toolchains `std::round` is not available (e.g. GCC 6). + using namespace std; + num_fractional_digits = + count_fractional_digits::value; + if (num_fractional_digits < 6 && static_cast(round(val)) != val) + num_fractional_digits = 6; + } + + format_to(std::back_inserter(buf), FMT_STRING("{:.{}f}"), + std::fmod(val * static_cast(Duration::period::num) / + static_cast(Duration::period::den), + static_cast(60)), + num_fractional_digits); +} + +template +class tm_writer { private: static constexpr int days_per_week = 7; const std::locale& loc_; const bool is_classic_; OutputIt out_; + const Duration* subsecs_; const std::tm& tm_; auto tm_sec() const noexcept -> int { @@ -1051,6 +1213,17 @@ template class tm_writer { *out_++ = *d++; *out_++ = *d; } + void write2(int value, pad_type pad) { + unsigned int v = to_unsigned(value) % 100; + if (v >= 10) { + const char* d = digits2(v); + *out_++ = *d++; + *out_++ = *d; + } else { + out_ = detail::write_padding(out_, pad); + *out_++ = static_cast('0' + v); + } + } void write_year_extended(long long year) { // At least 4 characters. @@ -1074,7 +1247,7 @@ template class tm_writer { } } - void write_utc_offset(long offset) { + void write_utc_offset(long offset, numeric_system ns) { if (offset < 0) { *out_++ = '-'; offset = -offset; @@ -1083,14 +1256,15 @@ template class tm_writer { } offset /= 60; write2(static_cast(offset / 60)); + if (ns != numeric_system::standard) *out_++ = ':'; write2(static_cast(offset % 60)); } template ::value)> - void format_utc_offset_impl(const T& tm) { - write_utc_offset(tm.tm_gmtoff); + void format_utc_offset_impl(const T& tm, numeric_system ns) { + write_utc_offset(tm.tm_gmtoff, ns); } template ::value)> - void format_utc_offset_impl(const T& tm) { + void format_utc_offset_impl(const T& tm, numeric_system ns) { #if defined(_WIN32) && defined(_UCRT) # if FMT_USE_TZSET tzset_once(); @@ -1102,10 +1276,17 @@ template class tm_writer { _get_dstbias(&dstbias); offset += dstbias; } - write_utc_offset(-offset); + write_utc_offset(-offset, ns); #else - ignore_unused(tm); - format_localized('z'); + if (ns == numeric_system::standard) return format_localized('z'); + + // Extract timezone offset from timezone conversion functions. + std::tm gtm = tm; + std::time_t gt = std::mktime(>m); + std::tm ltm = gmtime(gt); + std::time_t lt = std::mktime(<m); + long offset = gt - lt; + write_utc_offset(offset, ns); #endif } @@ -1126,10 +1307,12 @@ template class tm_writer { } public: - tm_writer(const std::locale& loc, OutputIt out, const std::tm& tm) + tm_writer(const std::locale& loc, OutputIt out, const std::tm& tm, + const Duration* subsecs = nullptr) : loc_(loc), is_classic_(loc_ == get_classic_locale()), out_(out), + subsecs_(subsecs), tm_(tm) {} OutputIt out() const { return out_; } @@ -1227,7 +1410,7 @@ template class tm_writer { out_ = copy_str(std::begin(buf) + offset, std::end(buf), out_); } - void on_utc_offset() { format_utc_offset_impl(tm_); } + void on_utc_offset(numeric_system ns) { format_utc_offset_impl(tm_, ns); } void on_tz_name() { format_tz_name_impl(tm_); } void on_year(numeric_system ns) { @@ -1315,22 +1498,41 @@ template class tm_writer { } } - void on_24_hour(numeric_system ns) { - if (is_classic_ || ns == numeric_system::standard) return write2(tm_hour()); + void on_24_hour(numeric_system ns, pad_type pad) { + if (is_classic_ || ns == numeric_system::standard) + return write2(tm_hour(), pad); format_localized('H', 'O'); } - void on_12_hour(numeric_system ns) { + void on_12_hour(numeric_system ns, pad_type pad) { if (is_classic_ || ns == numeric_system::standard) - return write2(tm_hour12()); + return write2(tm_hour12(), pad); format_localized('I', 'O'); } - void on_minute(numeric_system ns) { - if (is_classic_ || ns == numeric_system::standard) return write2(tm_min()); + void on_minute(numeric_system ns, pad_type pad) { + if (is_classic_ || ns == numeric_system::standard) + return write2(tm_min(), pad); format_localized('M', 'O'); } - void on_second(numeric_system ns) { - if (is_classic_ || ns == numeric_system::standard) return write2(tm_sec()); - format_localized('S', 'O'); + + void on_second(numeric_system ns, pad_type pad) { + if (is_classic_ || ns == numeric_system::standard) { + write2(tm_sec(), pad); + if (subsecs_) { + if (std::is_floating_point::value) { + auto buf = memory_buffer(); + write_floating_seconds(buf, *subsecs_); + if (buf.size() > 1) { + // Remove the leading "0", write something like ".123". + out_ = std::copy(buf.begin() + 1, buf.end(), out_); + } + } else { + write_fractional_seconds(out_, *subsecs_); + } + } + } else { + // Currently no formatting of subseconds when a locale is set. + format_localized('S', 'O'); + } } void on_12_hour_time() { @@ -1351,10 +1553,9 @@ template class tm_writer { write2(tm_min()); } void on_iso_time() { - char buf[8]; - write_digit2_separated(buf, to_unsigned(tm_hour()), to_unsigned(tm_min()), - to_unsigned(tm_sec()), ':'); - out_ = copy_str(std::begin(buf), std::end(buf), out_); + on_24_hour_time(); + *out_++ = ':'; + on_second(numeric_system::standard, pad_type::unspecified); } void on_am_pm() { @@ -1372,43 +1573,34 @@ template class tm_writer { }; struct chrono_format_checker : null_chrono_spec_handler { + bool has_precision_integral = false; + FMT_NORETURN void unsupported() { FMT_THROW(format_error("no date")); } template FMT_CONSTEXPR void on_text(const Char*, const Char*) {} - FMT_CONSTEXPR void on_24_hour(numeric_system) {} - FMT_CONSTEXPR void on_12_hour(numeric_system) {} - FMT_CONSTEXPR void on_minute(numeric_system) {} - FMT_CONSTEXPR void on_second(numeric_system) {} + FMT_CONSTEXPR void on_24_hour(numeric_system, pad_type) {} + FMT_CONSTEXPR void on_12_hour(numeric_system, pad_type) {} + FMT_CONSTEXPR void on_minute(numeric_system, pad_type) {} + FMT_CONSTEXPR void on_second(numeric_system, pad_type) {} FMT_CONSTEXPR void on_12_hour_time() {} FMT_CONSTEXPR void on_24_hour_time() {} FMT_CONSTEXPR void on_iso_time() {} FMT_CONSTEXPR void on_am_pm() {} - FMT_CONSTEXPR void on_duration_value() {} + FMT_CONSTEXPR void on_duration_value() const { + if (has_precision_integral) { + FMT_THROW(format_error("precision not allowed for this argument type")); + } + } FMT_CONSTEXPR void on_duration_unit() {} }; -template ::value)> +template ::value&& has_isfinite::value)> inline bool isfinite(T) { return true; } -// Converts value to Int and checks that it's in the range [0, upper). -template ::value)> -inline Int to_nonnegative_int(T value, Int upper) { - FMT_ASSERT(std::is_unsigned::value || - (value >= 0 && to_unsigned(value) <= to_unsigned(upper)), - "invalid value"); - (void)upper; - return static_cast(value); -} -template ::value)> -inline Int to_nonnegative_int(T value, Int upper) { - if (value < 0 || value > static_cast(upper)) - FMT_THROW(format_error("invalid value")); - return static_cast(value); -} - template ::value)> inline T mod(T x, int y) { return x % static_cast(y); @@ -1463,47 +1655,6 @@ inline std::chrono::duration get_milliseconds( #endif } -// Counts the number of fractional digits in the range [0, 18] according to the -// C++20 spec. If more than 18 fractional digits are required then returns 6 for -// microseconds precision. -template () / 10)> -struct count_fractional_digits { - static constexpr int value = - Num % Den == 0 ? N : count_fractional_digits::value; -}; - -// Base case that doesn't instantiate any more templates -// in order to avoid overflow. -template -struct count_fractional_digits { - static constexpr int value = (Num % Den == 0) ? N : 6; -}; - -constexpr long long pow10(std::uint32_t n) { - return n == 0 ? 1 : 10 * pow10(n - 1); -} - -template ::is_signed)> -constexpr std::chrono::duration abs( - std::chrono::duration d) { - // We need to compare the duration using the count() method directly - // due to a compiler bug in clang-11 regarding the spaceship operator, - // when -Wzero-as-null-pointer-constant is enabled. - // In clang-12 the bug has been fixed. See - // https://bugs.llvm.org/show_bug.cgi?id=46235 and the reproducible example: - // https://www.godbolt.org/z/Knbb5joYx. - return d.count() >= d.zero().count() ? d : -d; -} - -template ::is_signed)> -constexpr std::chrono::duration abs( - std::chrono::duration d) { - return d; -} - template ::value)> OutputIt format_duration_value(OutputIt out, Rep val, int) { @@ -1513,7 +1664,7 @@ OutputIt format_duration_value(OutputIt out, Rep val, int) { template ::value)> OutputIt format_duration_value(OutputIt out, Rep val, int precision) { - auto specs = basic_format_specs(); + auto specs = format_specs(); specs.precision = precision; specs.type = precision >= 0 ? presentation_type::fixed_lower : presentation_type::general_lower; @@ -1654,44 +1805,16 @@ struct chrono_formatter { } } - void write(Rep value, int width) { + void write(Rep value, int width, pad_type pad = pad_type::unspecified) { write_sign(); if (isnan(value)) return write_nan(); uint32_or_64_or_128_t n = to_unsigned(to_nonnegative_int(value, max_value())); int num_digits = detail::count_digits(n); - if (width > num_digits) out = std::fill_n(out, width - num_digits, '0'); - out = format_decimal(out, n, num_digits).end; - } - - template void write_fractional_seconds(Duration d) { - FMT_ASSERT(!std::is_floating_point::value, ""); - constexpr auto num_fractional_digits = - count_fractional_digits::value; - - using subsecond_precision = std::chrono::duration< - typename std::common_type::type, - std::ratio<1, detail::pow10(num_fractional_digits)>>; - if (std::ratio_less::value) { - *out++ = '.'; - auto fractional = - detail::abs(d) - std::chrono::duration_cast(d); - auto subseconds = - std::chrono::treat_as_floating_point< - typename subsecond_precision::rep>::value - ? fractional.count() - : std::chrono::duration_cast(fractional) - .count(); - uint32_or_64_or_128_t n = - to_unsigned(to_nonnegative_int(subseconds, max_value())); - int num_digits = detail::count_digits(n); - if (num_fractional_digits > num_digits) - out = std::fill_n(out, num_fractional_digits - num_digits, '0'); - out = format_decimal(out, n, num_digits).end; + if (width > num_digits) { + out = detail::write_padding(out, pad, width - num_digits); } + out = format_decimal(out, n, num_digits).end; } void write_nan() { std::copy_n("nan", 3, out); } @@ -1723,7 +1846,7 @@ struct chrono_formatter { void on_loc_time(numeric_system) {} void on_us_date() {} void on_iso_date() {} - void on_utc_offset() {} + void on_utc_offset(numeric_system) {} void on_tz_name() {} void on_year(numeric_system) {} void on_short_year(numeric_system) {} @@ -1739,58 +1862,56 @@ struct chrono_formatter { void on_day_of_month(numeric_system) {} void on_day_of_month_space(numeric_system) {} - void on_24_hour(numeric_system ns) { + void on_24_hour(numeric_system ns, pad_type pad) { if (handle_nan_inf()) return; - if (ns == numeric_system::standard) return write(hour(), 2); + if (ns == numeric_system::standard) return write(hour(), 2, pad); auto time = tm(); time.tm_hour = to_nonnegative_int(hour(), 24); - format_tm(time, &tm_writer_type::on_24_hour, ns); + format_tm(time, &tm_writer_type::on_24_hour, ns, pad); } - void on_12_hour(numeric_system ns) { + void on_12_hour(numeric_system ns, pad_type pad) { if (handle_nan_inf()) return; - if (ns == numeric_system::standard) return write(hour12(), 2); + if (ns == numeric_system::standard) return write(hour12(), 2, pad); auto time = tm(); time.tm_hour = to_nonnegative_int(hour12(), 12); - format_tm(time, &tm_writer_type::on_12_hour, ns); + format_tm(time, &tm_writer_type::on_12_hour, ns, pad); } - void on_minute(numeric_system ns) { + void on_minute(numeric_system ns, pad_type pad) { if (handle_nan_inf()) return; - if (ns == numeric_system::standard) return write(minute(), 2); + if (ns == numeric_system::standard) return write(minute(), 2, pad); auto time = tm(); time.tm_min = to_nonnegative_int(minute(), 60); - format_tm(time, &tm_writer_type::on_minute, ns); + format_tm(time, &tm_writer_type::on_minute, ns, pad); } - void on_second(numeric_system ns) { + void on_second(numeric_system ns, pad_type pad) { if (handle_nan_inf()) return; if (ns == numeric_system::standard) { if (std::is_floating_point::value) { - constexpr auto num_fractional_digits = - count_fractional_digits::value; auto buf = memory_buffer(); - format_to(std::back_inserter(buf), runtime("{:.{}f}"), - std::fmod(val * static_cast(Period::num) / - static_cast(Period::den), - static_cast(60)), - num_fractional_digits); + write_floating_seconds(buf, std::chrono::duration(val), + precision); if (negative) *out++ = '-'; - if (buf.size() < 2 || buf[1] == '.') *out++ = '0'; + if (buf.size() < 2 || buf[1] == '.') { + out = detail::write_padding(out, pad); + } out = std::copy(buf.begin(), buf.end(), out); } else { - write(second(), 2); - write_fractional_seconds(std::chrono::duration(val)); + write(second(), 2, pad); + write_fractional_seconds( + out, std::chrono::duration(val), precision); } return; } auto time = tm(); time.tm_sec = to_nonnegative_int(second(), 60); - format_tm(time, &tm_writer_type::on_second, ns); + format_tm(time, &tm_writer_type::on_second, ns, pad); } void on_12_hour_time() { @@ -1814,7 +1935,7 @@ struct chrono_formatter { on_24_hour_time(); *out++ = ':'; if (handle_nan_inf()) return; - on_second(numeric_system::standard); + on_second(numeric_system::standard, pad_type::unspecified); } void on_am_pm() { @@ -1833,7 +1954,7 @@ struct chrono_formatter { } }; -FMT_END_DETAIL_NAMESPACE +} // namespace detail #if defined(__cpp_lib_chrono) && __cpp_lib_chrono >= 201907 using weekday = std::chrono::weekday; @@ -1883,118 +2004,67 @@ template struct formatter { template struct formatter, Char> { private: - basic_format_specs specs; - int precision = -1; - using arg_ref_type = detail::arg_ref; - arg_ref_type width_ref; - arg_ref_type precision_ref; - bool localized = false; - basic_string_view format_str; - using duration = std::chrono::duration; - - struct spec_handler { - formatter& f; - basic_format_parse_context& context; - basic_string_view format_str; - - template FMT_CONSTEXPR arg_ref_type make_arg_ref(Id arg_id) { - context.check_arg_id(arg_id); - return arg_ref_type(arg_id); - } - - FMT_CONSTEXPR arg_ref_type make_arg_ref(basic_string_view arg_id) { - context.check_arg_id(arg_id); - return arg_ref_type(arg_id); - } - - FMT_CONSTEXPR arg_ref_type make_arg_ref(detail::auto_id) { - return arg_ref_type(context.next_arg_id()); - } + format_specs specs_; + detail::arg_ref width_ref_; + detail::arg_ref precision_ref_; + bool localized_ = false; + basic_string_view format_str_; - void on_error(const char* msg) { FMT_THROW(format_error(msg)); } - FMT_CONSTEXPR void on_fill(basic_string_view fill) { - f.specs.fill = fill; - } - FMT_CONSTEXPR void on_align(align_t align) { f.specs.align = align; } - FMT_CONSTEXPR void on_width(int width) { f.specs.width = width; } - FMT_CONSTEXPR void on_precision(int _precision) { - f.precision = _precision; - } - FMT_CONSTEXPR void end_precision() {} + public: + FMT_CONSTEXPR auto parse(basic_format_parse_context& ctx) + -> decltype(ctx.begin()) { + auto it = ctx.begin(), end = ctx.end(); + if (it == end || *it == '}') return it; - template FMT_CONSTEXPR void on_dynamic_width(Id arg_id) { - f.width_ref = make_arg_ref(arg_id); - } + it = detail::parse_align(it, end, specs_); + if (it == end) return it; - template FMT_CONSTEXPR void on_dynamic_precision(Id arg_id) { - f.precision_ref = make_arg_ref(arg_id); - } - }; + it = detail::parse_dynamic_spec(it, end, specs_.width, width_ref_, ctx); + if (it == end) return it; - using iterator = typename basic_format_parse_context::iterator; - struct parse_range { - iterator begin; - iterator end; - }; - - FMT_CONSTEXPR parse_range do_parse(basic_format_parse_context& ctx) { - auto begin = ctx.begin(), end = ctx.end(); - if (begin == end || *begin == '}') return {begin, begin}; - spec_handler handler{*this, ctx, format_str}; - begin = detail::parse_align(begin, end, handler); - if (begin == end) return {begin, begin}; - begin = detail::parse_width(begin, end, handler); - if (begin == end) return {begin, begin}; - if (*begin == '.') { - if (std::is_floating_point::value) - begin = detail::parse_precision(begin, end, handler); - else - handler.on_error("precision not allowed for this argument type"); + auto checker = detail::chrono_format_checker(); + if (*it == '.') { + checker.has_precision_integral = !std::is_floating_point::value; + it = detail::parse_precision(it, end, specs_.precision, precision_ref_, + ctx); } - if (begin != end && *begin == 'L') { - ++begin; - localized = true; + if (it != end && *it == 'L') { + localized_ = true; + ++it; } - end = detail::parse_chrono_format(begin, end, - detail::chrono_format_checker()); - return {begin, end}; - } - - public: - FMT_CONSTEXPR auto parse(basic_format_parse_context& ctx) - -> decltype(ctx.begin()) { - auto range = do_parse(ctx); - format_str = basic_string_view( - &*range.begin, detail::to_unsigned(range.end - range.begin)); - return range.end; + end = detail::parse_chrono_format(it, end, checker); + format_str_ = {it, detail::to_unsigned(end - it)}; + return end; } template - auto format(const duration& d, FormatContext& ctx) const + auto format(std::chrono::duration d, FormatContext& ctx) const -> decltype(ctx.out()) { - auto specs_copy = specs; - auto precision_copy = precision; - auto begin = format_str.begin(), end = format_str.end(); + auto specs = specs_; + auto precision = specs.precision; + specs.precision = -1; + auto begin = format_str_.begin(), end = format_str_.end(); // As a possible future optimization, we could avoid extra copying if width // is not specified. - basic_memory_buffer buf; + auto buf = basic_memory_buffer(); auto out = std::back_inserter(buf); - detail::handle_dynamic_spec(specs_copy.width, - width_ref, ctx); - detail::handle_dynamic_spec(precision_copy, - precision_ref, ctx); + detail::handle_dynamic_spec(specs.width, width_ref_, + ctx); + detail::handle_dynamic_spec(precision, + precision_ref_, ctx); if (begin == end || *begin == '}') { - out = detail::format_duration_value(out, d.count(), precision_copy); + out = detail::format_duration_value(out, d.count(), precision); detail::format_duration_unit(out); } else { - detail::chrono_formatter f( - ctx, out, d); - f.precision = precision_copy; - f.localized = localized; + using chrono_formatter = + detail::chrono_formatter; + auto f = chrono_formatter(ctx, out, d); + f.precision = precision; + f.localized = localized_; detail::parse_chrono_format(begin, end, f); } return detail::write( - ctx.out(), basic_string_view(buf.data(), buf.size()), specs_copy); + ctx.out(), basic_string_view(buf.data(), buf.size()), specs); } }; @@ -2002,68 +2072,137 @@ template struct formatter, Char> : formatter { FMT_CONSTEXPR formatter() { - basic_string_view default_specs = - detail::string_literal{}; - this->do_parse(default_specs.begin(), default_specs.end()); + this->format_str_ = detail::string_literal{}; } template - auto format(std::chrono::time_point val, + auto format(std::chrono::time_point val, FormatContext& ctx) const -> decltype(ctx.out()) { - return formatter::format(localtime(val), ctx); + using period = typename Duration::period; + if (detail::const_check( + period::num != 1 || period::den != 1 || + std::is_floating_point::value)) { + const auto epoch = val.time_since_epoch(); + auto subsecs = std::chrono::duration_cast( + epoch - std::chrono::duration_cast(epoch)); + + if (subsecs.count() < 0) { + auto second = + std::chrono::duration_cast(std::chrono::seconds(1)); + if (epoch.count() < ((Duration::min)() + second).count()) + FMT_THROW(format_error("duration is too small")); + subsecs += second; + val -= second; + } + + return formatter::do_format( + gmtime(std::chrono::time_point_cast(val)), ctx, + &subsecs); + } + + return formatter::format( + gmtime(std::chrono::time_point_cast(val)), ctx); } }; +#if FMT_USE_LOCAL_TIME +template +struct formatter, Char> + : formatter { + FMT_CONSTEXPR formatter() { + this->format_str_ = detail::string_literal{}; + } + + template + auto format(std::chrono::local_time val, FormatContext& ctx) const + -> decltype(ctx.out()) { + using period = typename Duration::period; + if (period::num != 1 || period::den != 1 || + std::is_floating_point::value) { + const auto epoch = val.time_since_epoch(); + const auto subsecs = std::chrono::duration_cast( + epoch - std::chrono::duration_cast(epoch)); + + return formatter::do_format( + localtime(std::chrono::time_point_cast(val)), + ctx, &subsecs); + } + + return formatter::format( + localtime(std::chrono::time_point_cast(val)), + ctx); + } +}; +#endif + +#if FMT_USE_UTC_TIME +template +struct formatter, + Char> + : formatter, + Char> { + template + auto format(std::chrono::time_point val, + FormatContext& ctx) const -> decltype(ctx.out()) { + return formatter< + std::chrono::time_point, + Char>::format(std::chrono::utc_clock::to_sys(val), ctx); + } +}; +#endif + template struct formatter { private: - enum class spec { - unknown, - year_month_day, - hh_mm_ss, - }; - spec spec_ = spec::unknown; - basic_string_view specs; + format_specs specs_; + detail::arg_ref width_ref_; protected: - template FMT_CONSTEXPR auto do_parse(It begin, It end) -> It { - if (begin != end && *begin == ':') ++begin; - end = detail::parse_chrono_format(begin, end, detail::tm_format_checker()); - // Replace default spec only if the new spec is not empty. - if (end != begin) specs = {begin, detail::to_unsigned(end - begin)}; - return end; + basic_string_view format_str_; + + template + auto do_format(const std::tm& tm, FormatContext& ctx, + const Duration* subsecs) const -> decltype(ctx.out()) { + auto specs = specs_; + auto buf = basic_memory_buffer(); + auto out = std::back_inserter(buf); + detail::handle_dynamic_spec(specs.width, width_ref_, + ctx); + + auto loc_ref = ctx.locale(); + detail::get_locale loc(static_cast(loc_ref), loc_ref); + auto w = + detail::tm_writer(loc, out, tm, subsecs); + detail::parse_chrono_format(format_str_.begin(), format_str_.end(), w); + return detail::write( + ctx.out(), basic_string_view(buf.data(), buf.size()), specs); } public: FMT_CONSTEXPR auto parse(basic_format_parse_context& ctx) -> decltype(ctx.begin()) { - auto end = this->do_parse(ctx.begin(), ctx.end()); - // basic_string_view<>::compare isn't constexpr before C++17. - if (specs.size() == 2 && specs[0] == Char('%')) { - if (specs[1] == Char('F')) - spec_ = spec::year_month_day; - else if (specs[1] == Char('T')) - spec_ = spec::hh_mm_ss; - } + auto it = ctx.begin(), end = ctx.end(); + if (it == end || *it == '}') return it; + + it = detail::parse_align(it, end, specs_); + if (it == end) return it; + + it = detail::parse_dynamic_spec(it, end, specs_.width, width_ref_, ctx); + if (it == end) return it; + + end = detail::parse_chrono_format(it, end, detail::tm_format_checker()); + // Replace the default format_str only if the new spec is not empty. + if (end != it) format_str_ = {it, detail::to_unsigned(end - it)}; return end; } template auto format(const std::tm& tm, FormatContext& ctx) const -> decltype(ctx.out()) { - const auto loc_ref = ctx.locale(); - detail::get_locale loc(static_cast(loc_ref), loc_ref); - auto w = detail::tm_writer(loc, ctx.out(), tm); - if (spec_ == spec::year_month_day) - w.on_iso_date(); - else if (spec_ == spec::hh_mm_ss) - w.on_iso_time(); - else - detail::parse_chrono_format(specs.begin(), specs.end(), w); - return w.out(); + return do_format(tm, ctx, nullptr); } }; -FMT_MODULE_EXPORT_END +FMT_END_EXPORT FMT_END_NAMESPACE #endif // FMT_CHRONO_H_ diff --git a/contrib/fmt/include/fmt/color.h b/contrib/fmt/include/fmt/color.h index 4c163277e..8697e1ca0 100644 --- a/contrib/fmt/include/fmt/color.h +++ b/contrib/fmt/include/fmt/color.h @@ -11,7 +11,7 @@ #include "format.h" FMT_BEGIN_NAMESPACE -FMT_MODULE_EXPORT_BEGIN +FMT_BEGIN_EXPORT enum class color : uint32_t { alice_blue = 0xF0F8FF, // rgb(240,248,255) @@ -203,7 +203,7 @@ struct rgb { uint8_t b; }; -FMT_BEGIN_DETAIL_NAMESPACE +namespace detail { // color is a struct of either a rgb color or a terminal color. struct color_type { @@ -225,8 +225,7 @@ struct color_type { uint32_t rgb_color; } value; }; - -FMT_END_DETAIL_NAMESPACE +} // namespace detail /** A text style consisting of foreground and background colors and emphasis. */ class text_style { @@ -323,7 +322,7 @@ FMT_CONSTEXPR inline text_style operator|(emphasis lhs, emphasis rhs) noexcept { return text_style(lhs) | rhs; } -FMT_BEGIN_DETAIL_NAMESPACE +namespace detail { template struct ansi_color_escape { FMT_CONSTEXPR ansi_color_escape(detail::color_type text_color, @@ -423,26 +422,6 @@ FMT_CONSTEXPR ansi_color_escape make_emphasis(emphasis em) noexcept { return ansi_color_escape(em); } -template inline void fputs(const Char* chars, FILE* stream) { - int result = std::fputs(chars, stream); - if (result < 0) - FMT_THROW(system_error(errno, FMT_STRING("cannot write to file"))); -} - -template <> inline void fputs(const wchar_t* chars, FILE* stream) { - int result = std::fputws(chars, stream); - if (result < 0) - FMT_THROW(system_error(errno, FMT_STRING("cannot write to file"))); -} - -template inline void reset_color(FILE* stream) { - fputs("\x1b[0m", stream); -} - -template <> inline void reset_color(FILE* stream) { - fputs(L"\x1b[0m", stream); -} - template inline void reset_color(buffer& buffer) { auto reset_color = string_view("\x1b[0m"); buffer.append(reset_color.begin(), reset_color.end()); @@ -477,19 +456,21 @@ void vformat_to(buffer& buf, const text_style& ts, if (has_style) detail::reset_color(buf); } -FMT_END_DETAIL_NAMESPACE +} // namespace detail -template > -void vprint(std::FILE* f, const text_style& ts, const S& format, - basic_format_args>> args) { - basic_memory_buffer buf; - detail::vformat_to(buf, ts, detail::to_string_view(format), args); +inline void vprint(std::FILE* f, const text_style& ts, string_view fmt, + format_args args) { + // Legacy wide streams are not supported. + auto buf = memory_buffer(); + detail::vformat_to(buf, ts, fmt, args); if (detail::is_utf8()) { - detail::print(f, basic_string_view(buf.begin(), buf.size())); - } else { - buf.push_back(Char(0)); - detail::fputs(buf.data(), f); + detail::print(f, string_view(buf.begin(), buf.size())); + return; } + buf.push_back('\0'); + int result = std::fputs(buf.data(), f); + if (result < 0) + FMT_THROW(system_error(errno, FMT_STRING("cannot write to file"))); } /** @@ -566,7 +547,7 @@ OutputIt vformat_to( basic_format_args>> args) { auto&& buf = detail::get_buffer(out); detail::vformat_to(buf, ts, format_str, args); - return detail::get_iterator(buf); + return detail::get_iterator(buf, out); } /** @@ -645,7 +626,7 @@ FMT_CONSTEXPR auto styled(const T& value, text_style ts) return detail::styled_arg>{value, ts}; } -FMT_MODULE_EXPORT_END +FMT_END_EXPORT FMT_END_NAMESPACE #endif // FMT_COLOR_H_ diff --git a/contrib/fmt/include/fmt/compile.h b/contrib/fmt/include/fmt/compile.h index 933668c41..a4c7e4956 100644 --- a/contrib/fmt/include/fmt/compile.h +++ b/contrib/fmt/include/fmt/compile.h @@ -19,84 +19,6 @@ FMT_CONSTEXPR inline counting_iterator copy_str(InputIt begin, InputIt end, return it + (end - begin); } -template class truncating_iterator_base { - protected: - OutputIt out_; - size_t limit_; - size_t count_ = 0; - - truncating_iterator_base() : out_(), limit_(0) {} - - truncating_iterator_base(OutputIt out, size_t limit) - : out_(out), limit_(limit) {} - - public: - using iterator_category = std::output_iterator_tag; - using value_type = typename std::iterator_traits::value_type; - using difference_type = std::ptrdiff_t; - using pointer = void; - using reference = void; - FMT_UNCHECKED_ITERATOR(truncating_iterator_base); - - OutputIt base() const { return out_; } - size_t count() const { return count_; } -}; - -// An output iterator that truncates the output and counts the number of objects -// written to it. -template ::value_type>::type> -class truncating_iterator; - -template -class truncating_iterator - : public truncating_iterator_base { - mutable typename truncating_iterator_base::value_type blackhole_; - - public: - using value_type = typename truncating_iterator_base::value_type; - - truncating_iterator() = default; - - truncating_iterator(OutputIt out, size_t limit) - : truncating_iterator_base(out, limit) {} - - truncating_iterator& operator++() { - if (this->count_++ < this->limit_) ++this->out_; - return *this; - } - - truncating_iterator operator++(int) { - auto it = *this; - ++*this; - return it; - } - - value_type& operator*() const { - return this->count_ < this->limit_ ? *this->out_ : blackhole_; - } -}; - -template -class truncating_iterator - : public truncating_iterator_base { - public: - truncating_iterator() = default; - - truncating_iterator(OutputIt out, size_t limit) - : truncating_iterator_base(out, limit) {} - - template truncating_iterator& operator=(T val) { - if (this->count_++ < this->limit_) *this->out_++ = val; - return *this; - } - - truncating_iterator& operator++() { return *this; } - truncating_iterator& operator++(int) { return *this; } - truncating_iterator& operator*() { return *this; } -}; - // A compile-time string which is compiled into fast formatting code. class compiled_string {}; @@ -196,7 +118,8 @@ template struct code_unit { template constexpr OutputIt format(OutputIt out, const Args&...) const { - return write(out, value); + *out++ = value; + return out; } }; @@ -220,7 +143,12 @@ template struct field { template constexpr OutputIt format(OutputIt out, const Args&... args) const { - return write(out, get_arg_checked(args...)); + const T& arg = get_arg_checked(args...); + if constexpr (std::is_convertible_v>) { + auto s = basic_string_view(arg); + return copy_str(s.begin(), s.end(), out); + } + return write(out, arg); } }; @@ -331,14 +259,14 @@ template struct parse_specs_result { int next_arg_id; }; -constexpr int manual_indexing_id = -1; +enum { manual_indexing_id = -1 }; template constexpr parse_specs_result parse_specs(basic_string_view str, size_t pos, int next_arg_id) { str.remove_prefix(pos); - auto ctx = compile_parse_context(str, max_value(), nullptr, {}, - next_arg_id); + auto ctx = + compile_parse_context(str, max_value(), nullptr, next_arg_id); auto f = formatter(); auto end = f.parse(ctx); return {f, pos + fmt::detail::to_unsigned(end - str.data()), @@ -348,22 +276,18 @@ constexpr parse_specs_result parse_specs(basic_string_view str, template struct arg_id_handler { arg_ref arg_id; - constexpr int operator()() { + constexpr int on_auto() { FMT_ASSERT(false, "handler cannot be used with automatic indexing"); return 0; } - constexpr int operator()(int id) { + constexpr int on_index(int id) { arg_id = arg_ref(id); return 0; } - constexpr int operator()(basic_string_view id) { + constexpr int on_name(basic_string_view id) { arg_id = arg_ref(id); return 0; } - - constexpr void on_error(const char* message) { - FMT_THROW(format_error(message)); - } }; template struct parse_arg_id_result { @@ -452,20 +376,18 @@ constexpr auto compile_format_string(S format_str) { } else if constexpr (arg_id_result.arg_id.kind == arg_id_kind::name) { constexpr auto arg_index = get_arg_index_by_name(arg_id_result.arg_id.val.name, Args{}); - if constexpr (arg_index != invalid_arg_index) { + if constexpr (arg_index >= 0) { constexpr auto next_id = ID != manual_indexing_id ? ID + 1 : manual_indexing_id; return parse_replacement_field_then_tail< decltype(get_type::value), Args, arg_id_end_pos, arg_index, next_id>(format_str); - } else { - if constexpr (c == '}') { - return parse_tail( - runtime_named_field{arg_id_result.arg_id.val.name}, - format_str); - } else if constexpr (c == ':') { - return unknown_format(); // no type info for specs parsing - } + } else if constexpr (c == '}') { + return parse_tail( + runtime_named_field{arg_id_result.arg_id.val.name}, + format_str); + } else if constexpr (c == ':') { + return unknown_format(); // no type info for specs parsing } } } @@ -501,7 +423,7 @@ constexpr auto compile(S format_str) { #endif // defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction) } // namespace detail -FMT_MODULE_EXPORT_BEGIN +FMT_BEGIN_EXPORT #if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction) @@ -568,9 +490,10 @@ template ::value)> format_to_n_result format_to_n(OutputIt out, size_t n, const S& format_str, Args&&... args) { - auto it = fmt::format_to(detail::truncating_iterator(out, n), - format_str, std::forward(args)...); - return {it.base(), it.count()}; + using traits = detail::fixed_buffer_traits; + auto buf = detail::iterator_buffer(out, n); + format_to(std::back_inserter(buf), format_str, std::forward(args)...); + return {buf.out(), buf.count()}; } template constexpr auto operator""_cf() { } // namespace literals #endif -FMT_MODULE_EXPORT_END +FMT_END_EXPORT FMT_END_NAMESPACE #endif // FMT_COMPILE_H_ diff --git a/contrib/fmt/include/fmt/core.h b/contrib/fmt/include/fmt/core.h index f6a37af9e..bfdca5f11 100644 --- a/contrib/fmt/include/fmt/core.h +++ b/contrib/fmt/include/fmt/core.h @@ -13,11 +13,12 @@ #include // std::strlen #include #include +#include // std::addressof #include #include // The fmt library version in the form major * 10000 + minor * 100 + patch. -#define FMT_VERSION 90100 +#define FMT_VERSION 100100 #if defined(__clang__) && !defined(__ibmxl__) # define FMT_CLANG_VERSION (__clang_major__ * 100 + __clang_minor__) @@ -69,9 +70,7 @@ # define FMT_HAS_FEATURE(x) 0 #endif -#if (defined(__has_include) || FMT_ICC_VERSION >= 1600 || \ - FMT_MSC_VERSION > 1900) && \ - !defined(__INTELLISENSE__) +#if defined(__has_include) || FMT_ICC_VERSION >= 1600 || FMT_MSC_VERSION > 1900 # define FMT_HAS_INCLUDE(x) __has_include(x) #else # define FMT_HAS_INCLUDE(x) 0 @@ -94,7 +93,7 @@ #ifndef FMT_USE_CONSTEXPR # if (FMT_HAS_FEATURE(cxx_relaxed_constexpr) || FMT_MSC_VERSION >= 1912 || \ (FMT_GCC_VERSION >= 600 && FMT_CPLUSPLUS >= 201402L)) && \ - !FMT_ICC_VERSION && !defined(__NVCC__) + !FMT_ICC_VERSION && (!defined(__NVCC__) || FMT_CPLUSPLUS >= 202002L) # define FMT_USE_CONSTEXPR 1 # else # define FMT_USE_CONSTEXPR 0 @@ -140,22 +139,7 @@ # endif #endif -#ifndef FMT_DEPRECATED -# if FMT_HAS_CPP14_ATTRIBUTE(deprecated) || FMT_MSC_VERSION >= 1900 -# define FMT_DEPRECATED [[deprecated]] -# else -# if (defined(__GNUC__) && !defined(__LCC__)) || defined(__clang__) -# define FMT_DEPRECATED __attribute__((deprecated)) -# elif FMT_MSC_VERSION -# define FMT_DEPRECATED __declspec(deprecated) -# else -# define FMT_DEPRECATED /* deprecated */ -# endif -# endif -#endif - -// [[noreturn]] is disabled on MSVC and NVCC because of bogus unreachable code -// warnings. +// Disable [[noreturn]] on MSVC/NVCC because of bogus unreachable code warnings. #if FMT_EXCEPTIONS && FMT_HAS_CPP_ATTRIBUTE(noreturn) && !FMT_MSC_VERSION && \ !defined(__NVCC__) # define FMT_NORETURN [[noreturn]] @@ -163,17 +147,6 @@ # define FMT_NORETURN #endif -#if FMT_HAS_CPP17_ATTRIBUTE(fallthrough) -# define FMT_FALLTHROUGH [[fallthrough]] -#elif defined(__clang__) -# define FMT_FALLTHROUGH [[clang::fallthrough]] -#elif FMT_GCC_VERSION >= 700 && \ - (!defined(__EDG_VERSION__) || __EDG_VERSION__ >= 520) -# define FMT_FALLTHROUGH [[gnu::fallthrough]] -#else -# define FMT_FALLTHROUGH -#endif - #ifndef FMT_NODISCARD # if FMT_HAS_CPP17_ATTRIBUTE(nodiscard) # define FMT_NODISCARD [[nodiscard]] @@ -182,16 +155,6 @@ # endif #endif -#ifndef FMT_USE_FLOAT -# define FMT_USE_FLOAT 1 -#endif -#ifndef FMT_USE_DOUBLE -# define FMT_USE_DOUBLE 1 -#endif -#ifndef FMT_USE_LONG_DOUBLE -# define FMT_USE_LONG_DOUBLE 1 -#endif - #ifndef FMT_INLINE # if FMT_GCC_VERSION || FMT_CLANG_VERSION # define FMT_INLINE inline __attribute__((always_inline)) @@ -200,9 +163,6 @@ # endif #endif -// An inline std::forward replacement. -#define FMT_FORWARD(...) static_cast(__VA_ARGS__) - #ifdef _MSC_VER # define FMT_UNCHECKED_ITERATOR(It) \ using _Unchecked_type = It // Mark iterator as checked. @@ -213,30 +173,26 @@ #ifndef FMT_BEGIN_NAMESPACE # define FMT_BEGIN_NAMESPACE \ namespace fmt { \ - inline namespace v9 { + inline namespace v10 { # define FMT_END_NAMESPACE \ } \ } #endif -#ifndef FMT_MODULE_EXPORT -# define FMT_MODULE_EXPORT -# define FMT_MODULE_EXPORT_BEGIN -# define FMT_MODULE_EXPORT_END -# define FMT_BEGIN_DETAIL_NAMESPACE namespace detail { -# define FMT_END_DETAIL_NAMESPACE } +#ifndef FMT_EXPORT +# define FMT_EXPORT +# define FMT_BEGIN_EXPORT +# define FMT_END_EXPORT #endif #if !defined(FMT_HEADER_ONLY) && defined(_WIN32) -# define FMT_CLASS_API FMT_MSC_WARNING(suppress : 4275) -# ifdef FMT_EXPORT +# ifdef FMT_LIB_EXPORT # define FMT_API __declspec(dllexport) # elif defined(FMT_SHARED) # define FMT_API __declspec(dllimport) # endif #else -# define FMT_CLASS_API -# if defined(FMT_EXPORT) || defined(FMT_SHARED) +# if defined(FMT_LIB_EXPORT) || defined(FMT_SHARED) # if defined(__GNUC__) || defined(__clang__) # define FMT_API __attribute__((visibility("default"))) # endif @@ -261,11 +217,13 @@ #endif #ifndef FMT_CONSTEVAL -# if ((FMT_GCC_VERSION >= 1000 || FMT_CLANG_VERSION >= 1101) && \ - FMT_CPLUSPLUS >= 202002L && !defined(__apple_build_version__)) || \ - (defined(__cpp_consteval) && \ +# if ((FMT_GCC_VERSION >= 1000 || FMT_CLANG_VERSION >= 1101) && \ + (!defined(__apple_build_version__) || \ + __apple_build_version__ >= 14000029L) && \ + FMT_CPLUSPLUS >= 202002L) || \ + (defined(__cpp_consteval) && \ (!FMT_MSC_VERSION || _MSC_FULL_VER >= 193030704)) -// consteval is broken in MSVC before VS2022 and Apple clang 13. +// consteval is broken in MSVC before VS2022 and Apple clang before 14. # define FMT_CONSTEVAL consteval # define FMT_HAS_CONSTEVAL # else @@ -277,7 +235,7 @@ # if defined(__cpp_nontype_template_args) && \ ((FMT_GCC_VERSION >= 903 && FMT_CPLUSPLUS >= 201709L) || \ __cpp_nontype_template_args >= 201911L) && \ - !defined(__NVCOMPILER) + !defined(__NVCOMPILER) && !defined(__LCC__) # define FMT_USE_NONTYPE_TEMPLATE_ARGS 1 # else # define FMT_USE_NONTYPE_TEMPLATE_ARGS 0 @@ -286,12 +244,12 @@ // Enable minimal optimizations for more compact code in debug mode. FMT_GCC_PRAGMA("GCC push_options") -#if !defined(__OPTIMIZE__) && !defined(__NVCOMPILER) +#if !defined(__OPTIMIZE__) && !defined(__NVCOMPILER) && !defined(__LCC__) && \ + !defined(__CUDACC__) FMT_GCC_PRAGMA("GCC optimize(\"Og\")") #endif FMT_BEGIN_NAMESPACE -FMT_MODULE_EXPORT_BEGIN // Implementations of enable_if_t and other metafunctions for older systems. template @@ -310,17 +268,10 @@ template using type_identity_t = typename type_identity::type; template using underlying_t = typename std::underlying_type::type; -template struct disjunction : std::false_type {}; -template struct disjunction

: P {}; -template -struct disjunction - : conditional_t> {}; - -template struct conjunction : std::true_type {}; -template struct conjunction

: P {}; -template -struct conjunction - : conditional_t, P1> {}; +// Checks whether T is a container with contiguous storage. +template struct is_contiguous : std::false_type {}; +template +struct is_contiguous> : std::true_type {}; struct monostate { constexpr monostate() {} @@ -332,11 +283,19 @@ struct monostate { #ifdef FMT_DOC # define FMT_ENABLE_IF(...) #else -# define FMT_ENABLE_IF(...) enable_if_t<(__VA_ARGS__), int> = 0 +# define FMT_ENABLE_IF(...) fmt::enable_if_t<(__VA_ARGS__), int> = 0 #endif -FMT_BEGIN_DETAIL_NAMESPACE +// This is defined in core.h instead of format.h to avoid injecting in std. +// It is a template to avoid undesirable implicit conversions to std::byte. +#ifdef __cpp_lib_byte +template ::value)> +inline auto format_as(T b) -> unsigned char { + return static_cast(b); +} +#endif +namespace detail { // Suppresses "unused variable" warnings with the method described in // https://herbsutter.com/2009/10/18/mailbag-shutting-up-compiler-warnings/. // (void)var does not work on many Intel compilers. @@ -344,7 +303,15 @@ template FMT_CONSTEXPR void ignore_unused(const T&...) {} constexpr FMT_INLINE auto is_constant_evaluated( bool default_value = false) noexcept -> bool { -#ifdef __cpp_lib_is_constant_evaluated +// Workaround for incompatibility between libstdc++ consteval-based +// std::is_constant_evaluated() implementation and clang-14. +// https://github.com/fmtlib/fmt/issues/3247 +#if FMT_CPLUSPLUS >= 202002L && defined(_GLIBCXX_RELEASE) && \ + _GLIBCXX_RELEASE >= 12 && \ + (FMT_CLANG_VERSION >= 1400 && FMT_CLANG_VERSION < 1500) + ignore_unused(default_value); + return __builtin_is_constant_evaluated(); +#elif defined(__cpp_lib_is_constant_evaluated) ignore_unused(default_value); return std::is_constant_evaluated(); #else @@ -364,12 +331,12 @@ FMT_NORETURN FMT_API void assert_fail(const char* file, int line, # ifdef NDEBUG // FMT_ASSERT is not empty to avoid -Wempty-body. # define FMT_ASSERT(condition, message) \ - ::fmt::detail::ignore_unused((condition), (message)) + fmt::detail::ignore_unused((condition), (message)) # else # define FMT_ASSERT(condition, message) \ ((condition) /* void() fails with -Winvalid-constexpr on clang 4.0.1 */ \ ? (void)0 \ - : ::fmt::detail::assert_fail(__FILE__, __LINE__, (message))) + : fmt::detail::assert_fail(__FILE__, __LINE__, (message))) # endif #endif @@ -410,15 +377,15 @@ FMT_CONSTEXPR auto to_unsigned(Int value) -> return static_cast::type>(value); } -FMT_MSC_WARNING(suppress : 4566) constexpr unsigned char micro[] = "\u00B5"; +FMT_CONSTEXPR inline auto is_utf8() -> bool { + FMT_MSC_WARNING(suppress : 4566) constexpr unsigned char section[] = "\u00A7"; -constexpr auto is_utf8() -> bool { // Avoid buggy sign extensions in MSVC's constant evaluation mode (#2297). using uchar = unsigned char; - return FMT_UNICODE || (sizeof(micro) == 3 && uchar(micro[0]) == 0xC2 && - uchar(micro[1]) == 0xB5); + return FMT_UNICODE || (sizeof(section) == 3 && uchar(section[0]) == 0xC2 && + uchar(section[1]) == 0xA7); } -FMT_END_DETAIL_NAMESPACE +} // namespace detail /** An implementation of ``std::basic_string_view`` for pre-C++17. It provides a @@ -427,6 +394,7 @@ FMT_END_DETAIL_NAMESPACE compiled with a different ``-std`` option than the client code (which is not recommended). */ +FMT_EXPORT template class basic_string_view { private: const Char* data_; @@ -486,6 +454,18 @@ template class basic_string_view { size_ -= n; } + FMT_CONSTEXPR_CHAR_TRAITS bool starts_with( + basic_string_view sv) const noexcept { + return size_ >= sv.size_ && + std::char_traits::compare(data_, sv.data_, sv.size_) == 0; + } + FMT_CONSTEXPR_CHAR_TRAITS bool starts_with(Char c) const noexcept { + return size_ >= 1 && std::char_traits::eq(*data_, c); + } + FMT_CONSTEXPR_CHAR_TRAITS bool starts_with(const Char* s) const { + return starts_with(basic_string_view(s)); + } + // Lexicographically compare this string reference to other. FMT_CONSTEXPR_CHAR_TRAITS auto compare(basic_string_view other) const -> int { size_t str_size = size_ < other.size_ ? size_ : other.size_; @@ -517,13 +497,15 @@ template class basic_string_view { } }; +FMT_EXPORT using string_view = basic_string_view; /** Specifies if ``T`` is a character type. Can be specialized by users. */ +FMT_EXPORT template struct is_char : std::false_type {}; template <> struct is_char : std::true_type {}; -FMT_BEGIN_DETAIL_NAMESPACE +namespace detail { // A base class for compile-time strings. struct compile_string {}; @@ -531,7 +513,6 @@ struct compile_string {}; template struct is_compile_string : std::is_base_of {}; -// Returns a string view of `s`. template ::value)> FMT_INLINE auto to_string_view(const Char* s) -> basic_string_view { return s; @@ -561,10 +542,10 @@ void to_string_view(...); // Specifies whether S is a string type convertible to fmt::basic_string_view. // It should be a constexpr function but MSVC 2017 fails to compile it in // enable_if and MSVC 2015 fails to compile it as an alias template. -// ADL invocation of to_string_view is DEPRECATED! +// ADL is intentionally disabled as to_string_view is not an extension point. template -struct is_string : std::is_class()))> { -}; +struct is_string + : std::is_class()))> {}; template struct char_t_impl {}; template struct char_t_impl::value>> { @@ -622,23 +603,44 @@ FMT_TYPE_CONSTANT(const void*, pointer_type); constexpr bool is_integral_type(type t) { return t > type::none_type && t <= type::last_integer_type; } - constexpr bool is_arithmetic_type(type t) { return t > type::none_type && t <= type::last_numeric_type; } +constexpr auto set(type rhs) -> int { return 1 << static_cast(rhs); } +constexpr auto in(type t, int set) -> bool { + return ((set >> static_cast(t)) & 1) != 0; +} + +// Bitsets of types. +enum { + sint_set = + set(type::int_type) | set(type::long_long_type) | set(type::int128_type), + uint_set = set(type::uint_type) | set(type::ulong_long_type) | + set(type::uint128_type), + bool_set = set(type::bool_type), + char_set = set(type::char_type), + float_set = set(type::float_type) | set(type::double_type) | + set(type::long_double_type), + string_set = set(type::string_type), + cstring_set = set(type::cstring_type), + pointer_set = set(type::pointer_type) +}; + FMT_NORETURN FMT_API void throw_format_error(const char* message); struct error_handler { constexpr error_handler() = default; - constexpr error_handler(const error_handler&) = default; // This function is intentionally not constexpr to give a compile-time error. FMT_NORETURN void on_error(const char* message) { throw_format_error(message); } }; -FMT_END_DETAIL_NAMESPACE +} // namespace detail + +/** Throws ``format_error`` with a given message. */ +using detail::throw_format_error; /** String's character type. */ template using char_t = typename detail::char_t_impl::type; @@ -650,8 +652,8 @@ template using char_t = typename detail::char_t_impl::type; You can use the ``format_parse_context`` type alias for ``char`` instead. \endrst */ -template -class basic_format_parse_context : private ErrorHandler { +FMT_EXPORT +template class basic_format_parse_context { private: basic_string_view format_str_; int next_arg_id_; @@ -660,12 +662,11 @@ class basic_format_parse_context : private ErrorHandler { public: using char_type = Char; - using iterator = typename basic_string_view::iterator; + using iterator = const Char*; explicit constexpr basic_format_parse_context( - basic_string_view format_str, ErrorHandler eh = {}, - int next_arg_id = 0) - : ErrorHandler(eh), format_str_(format_str), next_arg_id_(next_arg_id) {} + basic_string_view format_str, int next_arg_id = 0) + : format_str_(format_str), next_arg_id_(next_arg_id) {} /** Returns an iterator to the beginning of the format string range being @@ -691,7 +692,8 @@ class basic_format_parse_context : private ErrorHandler { */ FMT_CONSTEXPR auto next_arg_id() -> int { if (next_arg_id_ < 0) { - on_error("cannot switch from manual to automatic argument indexing"); + detail::throw_format_error( + "cannot switch from manual to automatic argument indexing"); return 0; } int id = next_arg_id_++; @@ -705,7 +707,8 @@ class basic_format_parse_context : private ErrorHandler { */ FMT_CONSTEXPR void check_arg_id(int id) { if (next_arg_id_ > 0) { - on_error("cannot switch from automatic to manual argument indexing"); + detail::throw_format_error( + "cannot switch from automatic to manual argument indexing"); return; } next_arg_id_ = -1; @@ -713,117 +716,50 @@ class basic_format_parse_context : private ErrorHandler { } FMT_CONSTEXPR void check_arg_id(basic_string_view) {} FMT_CONSTEXPR void check_dynamic_spec(int arg_id); - - FMT_CONSTEXPR void on_error(const char* message) { - ErrorHandler::on_error(message); - } - - constexpr auto error_handler() const -> ErrorHandler { return *this; } }; +FMT_EXPORT using format_parse_context = basic_format_parse_context; -FMT_BEGIN_DETAIL_NAMESPACE +namespace detail { // A parse context with extra data used only in compile-time checks. -template -class compile_parse_context - : public basic_format_parse_context { +template +class compile_parse_context : public basic_format_parse_context { private: int num_args_; const type* types_; - using base = basic_format_parse_context; + using base = basic_format_parse_context; public: explicit FMT_CONSTEXPR compile_parse_context( basic_string_view format_str, int num_args, const type* types, - ErrorHandler eh = {}, int next_arg_id = 0) - : base(format_str, eh, next_arg_id), num_args_(num_args), types_(types) {} + int next_arg_id = 0) + : base(format_str, next_arg_id), num_args_(num_args), types_(types) {} constexpr auto num_args() const -> int { return num_args_; } constexpr auto arg_type(int id) const -> type { return types_[id]; } FMT_CONSTEXPR auto next_arg_id() -> int { int id = base::next_arg_id(); - if (id >= num_args_) this->on_error("argument not found"); + if (id >= num_args_) throw_format_error("argument not found"); return id; } FMT_CONSTEXPR void check_arg_id(int id) { base::check_arg_id(id); - if (id >= num_args_) this->on_error("argument not found"); + if (id >= num_args_) throw_format_error("argument not found"); } using base::check_arg_id; FMT_CONSTEXPR void check_dynamic_spec(int arg_id) { + detail::ignore_unused(arg_id); +#if !defined(__LCC__) if (arg_id < num_args_ && types_ && !is_integral_type(types_[arg_id])) - this->on_error("width/precision is not integer"); - } -}; -FMT_END_DETAIL_NAMESPACE - -template -FMT_CONSTEXPR void -basic_format_parse_context::do_check_arg_id(int id) { - // Argument id is only checked at compile-time during parsing because - // formatting has its own validation. - if (detail::is_constant_evaluated() && FMT_GCC_VERSION >= 1200) { - using context = detail::compile_parse_context; - if (id >= static_cast(this)->num_args()) - on_error("argument not found"); - } -} - -template -FMT_CONSTEXPR void -basic_format_parse_context::check_dynamic_spec(int arg_id) { - if (detail::is_constant_evaluated()) { - using context = detail::compile_parse_context; - static_cast(this)->check_dynamic_spec(arg_id); + throw_format_error("width/precision is not integer"); +#endif } -} - -template class basic_format_arg; -template class basic_format_args; -template class dynamic_format_arg_store; - -// A formatter for objects of type T. -template -struct formatter { - // A deleted default constructor indicates a disabled formatter. - formatter() = delete; }; -// Specifies if T has an enabled formatter specialization. A type can be -// formattable even if it doesn't have a formatter e.g. via a conversion. -template -using has_formatter = - std::is_constructible>; - -// Checks whether T is a container with contiguous storage. -template struct is_contiguous : std::false_type {}; -template -struct is_contiguous> : std::true_type {}; - -class appender; - -FMT_BEGIN_DETAIL_NAMESPACE - -template -constexpr auto has_const_formatter_impl(T*) - -> decltype(typename Context::template formatter_type().format( - std::declval(), std::declval()), - true) { - return true; -} -template -constexpr auto has_const_formatter_impl(...) -> bool { - return false; -} -template -constexpr auto has_const_formatter() -> bool { - return has_const_formatter_impl(static_cast(nullptr)); -} - // Extracts a reference to the container from back_insert_iterator. template inline auto get_container(std::back_insert_iterator it) @@ -849,7 +785,7 @@ template U* { if (is_constant_evaluated()) return copy_str(begin, end, out); auto size = to_unsigned(end - begin); - memcpy(out, begin, size * sizeof(U)); + if (size > 0) memcpy(out, begin, size * sizeof(U)); return out + size; } @@ -892,11 +828,11 @@ template class buffer { buffer(const buffer&) = delete; void operator=(const buffer&) = delete; - auto begin() noexcept -> T* { return ptr_; } - auto end() noexcept -> T* { return ptr_ + size_; } + FMT_INLINE auto begin() noexcept -> T* { return ptr_; } + FMT_INLINE auto end() noexcept -> T* { return ptr_ + size_; } - auto begin() const noexcept -> const T* { return ptr_; } - auto end() const noexcept -> const T* { return ptr_ + size_; } + FMT_INLINE auto begin() const noexcept -> const T* { return ptr_; } + FMT_INLINE auto end() const noexcept -> const T* { return ptr_ + size_; } /** Returns the size of this buffer. */ constexpr auto size() const noexcept -> size_t { return size_; } @@ -904,10 +840,8 @@ template class buffer { /** Returns the capacity of this buffer. */ constexpr auto capacity() const noexcept -> size_t { return capacity_; } - /** Returns a pointer to the buffer data. */ + /** Returns a pointer to the buffer data (not null-terminated). */ FMT_CONSTEXPR auto data() noexcept -> T* { return ptr_; } - - /** Returns a pointer to the buffer data. */ FMT_CONSTEXPR auto data() const noexcept -> const T* { return ptr_; } /** Clears this buffer. */ @@ -1100,6 +1034,79 @@ template class counting_buffer final : public buffer { auto count() -> size_t { return count_ + this->size(); } }; +} // namespace detail + +template +FMT_CONSTEXPR void basic_format_parse_context::do_check_arg_id(int id) { + // Argument id is only checked at compile-time during parsing because + // formatting has its own validation. + if (detail::is_constant_evaluated() && + (!FMT_GCC_VERSION || FMT_GCC_VERSION >= 1200)) { + using context = detail::compile_parse_context; + if (id >= static_cast(this)->num_args()) + detail::throw_format_error("argument not found"); + } +} + +template +FMT_CONSTEXPR void basic_format_parse_context::check_dynamic_spec( + int arg_id) { + if (detail::is_constant_evaluated() && + (!FMT_GCC_VERSION || FMT_GCC_VERSION >= 1200)) { + using context = detail::compile_parse_context; + static_cast(this)->check_dynamic_spec(arg_id); + } +} + +FMT_EXPORT template class basic_format_arg; +FMT_EXPORT template class basic_format_args; +FMT_EXPORT template class dynamic_format_arg_store; + +// A formatter for objects of type T. +FMT_EXPORT +template +struct formatter { + // A deleted default constructor indicates a disabled formatter. + formatter() = delete; +}; + +// Specifies if T has an enabled formatter specialization. A type can be +// formattable even if it doesn't have a formatter e.g. via a conversion. +template +using has_formatter = + std::is_constructible>; + +// An output iterator that appends to a buffer. +// It is used to reduce symbol sizes for the common case. +class appender : public std::back_insert_iterator> { + using base = std::back_insert_iterator>; + + public: + using std::back_insert_iterator>::back_insert_iterator; + appender(base it) noexcept : base(it) {} + FMT_UNCHECKED_ITERATOR(appender); + + auto operator++() noexcept -> appender& { return *this; } + auto operator++(int) noexcept -> appender { return *this; } +}; + +namespace detail { + +template +constexpr auto has_const_formatter_impl(T*) + -> decltype(typename Context::template formatter_type().format( + std::declval(), std::declval()), + true) { + return true; +} +template +constexpr auto has_const_formatter_impl(...) -> bool { + return false; +} +template +constexpr auto has_const_formatter() -> bool { + return has_const_formatter_impl(static_cast(nullptr)); +} template using buffer_appender = conditional_t::value, appender, @@ -1110,29 +1117,21 @@ template auto get_buffer(OutputIt out) -> iterator_buffer { return iterator_buffer(out); } +template , Buf>::value)> +auto get_buffer(std::back_insert_iterator out) -> buffer& { + return get_container(out); +} -template -auto get_iterator(Buffer& buf) -> decltype(buf.out()) { +template +FMT_INLINE auto get_iterator(Buf& buf, OutputIt) -> decltype(buf.out()) { return buf.out(); } -template auto get_iterator(buffer& buf) -> buffer_appender { - return buffer_appender(buf); +template +auto get_iterator(buffer&, OutputIt out) -> OutputIt { + return out; } -template -struct fallback_formatter { - fallback_formatter() = delete; -}; - -// Specifies if T has an enabled fallback_formatter specialization. -template -using has_fallback_formatter = -#ifdef FMT_DEPRECATED_OSTREAM - std::is_constructible>; -#else - std::false_type; -#endif - struct view {}; template struct named_arg : view { @@ -1217,7 +1216,6 @@ constexpr auto count_statically_named_args() -> size_t { struct unformattable {}; struct unformattable_char : unformattable {}; -struct unformattable_const : unformattable {}; struct unformattable_pointer : unformattable {}; template struct string_value { @@ -1284,21 +1282,17 @@ template class value { FMT_INLINE value(const named_arg_info* args, size_t size) : named_args{args, size} {} - template FMT_CONSTEXPR FMT_INLINE value(T& val) { - using value_type = remove_cvref_t; - custom.value = const_cast(&val); + template FMT_CONSTEXPR20 FMT_INLINE value(T& val) { + using value_type = remove_const_t; + custom.value = const_cast(std::addressof(val)); // Get the formatter type through the context to allow different contexts // have different extension points, e.g. `formatter` for `format` and // `printf_formatter` for `printf`. custom.format = format_custom_arg< - value_type, - conditional_t::value, - typename Context::template formatter_type, - fallback_formatter>>; + value_type, typename Context::template formatter_type>; } value(unformattable); value(unformattable_char); - value(unformattable_const); value(unformattable_pointer); private: @@ -1315,29 +1309,25 @@ template class value { } }; -template -FMT_CONSTEXPR auto make_arg(T&& value) -> basic_format_arg; - // To minimize the number of types we need to deal with, long is translated // either to int or to long long depending on its size. enum { long_short = sizeof(long) == sizeof(int) }; using long_type = conditional_t; using ulong_type = conditional_t; -#ifdef __cpp_lib_byte -inline auto format_as(std::byte b) -> unsigned char { - return static_cast(b); -} -#endif - -template struct has_format_as { - template ::value&& std::is_integral::value)> - static auto check(U*) -> std::true_type; - static auto check(...) -> std::false_type; +template struct format_as_result { + template ::value || std::is_class::value)> + static auto map(U*) -> decltype(format_as(std::declval())); + static auto map(...) -> void; - enum { value = decltype(check(static_cast(nullptr)))::value }; + using type = decltype(map(static_cast(nullptr))); }; +template using format_as_t = typename format_as_result::type; + +template +struct has_format_as + : bool_constant, void>::value> {}; // Maps formatting arguments to core types. // arg_mapper reports errors by returning unformattable instead of using @@ -1414,25 +1404,6 @@ template struct arg_mapper { FMT_CONSTEXPR FMT_INLINE auto map(const T&) -> unformattable_char { return {}; } - template >::value && - !is_string::value && !has_formatter::value && - !has_fallback_formatter::value)> - FMT_CONSTEXPR FMT_INLINE auto map(const T& val) - -> basic_string_view { - return basic_string_view(val); - } - template >::value && - !std::is_convertible>::value && - !is_string::value && !has_formatter::value && - !has_fallback_formatter::value)> - FMT_CONSTEXPR FMT_INLINE auto map(const T& val) - -> basic_string_view { - return std_string_view(val); - } FMT_CONSTEXPR FMT_INLINE auto map(void* val) -> const void* { return val; } FMT_CONSTEXPR FMT_INLINE auto map(const void* val) -> const void* { @@ -1442,8 +1413,8 @@ template struct arg_mapper { return val; } - // We use SFINAE instead of a const T* parameter to avoid conflicting with - // the C array overload. + // Use SFINAE instead of a const T* parameter to avoid a conflict with the + // array overload. template < typename T, FMT_ENABLE_IF( @@ -1462,62 +1433,40 @@ template struct arg_mapper { return values; } - template ::value&& std::is_convertible::value && - !has_format_as::value && !has_formatter::value && - !has_fallback_formatter::value)> - FMT_DEPRECATED FMT_CONSTEXPR FMT_INLINE auto map(const T& val) - -> decltype(std::declval().map( - static_cast>(val))) { - return map(static_cast>(val)); - } - - template ::value && - !has_formatter::value)> - FMT_CONSTEXPR FMT_INLINE auto map(const T& val) - -> decltype(std::declval().map(format_as(T()))) { + // Only map owning types because mapping views can be unsafe. + template , + FMT_ENABLE_IF(std::is_arithmetic::value)> + FMT_CONSTEXPR FMT_INLINE auto map(const T& val) -> decltype(this->map(U())) { return map(format_as(val)); } - template > - struct formattable - : bool_constant() || - !std::is_const>::value || - has_fallback_formatter::value> {}; + template > + struct formattable : bool_constant() || + (has_formatter::value && + !std::is_const::value)> {}; -#if (FMT_MSC_VERSION != 0 && FMT_MSC_VERSION < 1910) || \ - FMT_ICC_VERSION != 0 || defined(__NVCC__) - // Workaround a bug in MSVC and Intel (Issue 2746). - template FMT_CONSTEXPR FMT_INLINE auto do_map(T&& val) -> T& { - return val; - } -#else template ::value)> - FMT_CONSTEXPR FMT_INLINE auto do_map(T&& val) -> T& { + FMT_CONSTEXPR FMT_INLINE auto do_map(T& val) -> T& { return val; } template ::value)> - FMT_CONSTEXPR FMT_INLINE auto do_map(T&&) -> unformattable_const { + FMT_CONSTEXPR FMT_INLINE auto do_map(T&) -> unformattable { return {}; } -#endif - template , - FMT_ENABLE_IF(!is_string::value && !is_char::value && - !std::is_array::value && - !std::is_pointer::value && - !has_format_as::value && - (has_formatter::value || - has_fallback_formatter::value))> - FMT_CONSTEXPR FMT_INLINE auto map(T&& val) - -> decltype(this->do_map(std::forward(val))) { - return do_map(std::forward(val)); + template , + FMT_ENABLE_IF((std::is_class::value || std::is_enum::value || + std::is_union::value) && + !is_string::value && !is_char::value && + !is_named_arg::value && + !std::is_arithmetic>::value)> + FMT_CONSTEXPR FMT_INLINE auto map(T& val) -> decltype(this->do_map(val)) { + return do_map(val); } template ::value)> FMT_CONSTEXPR FMT_INLINE auto map(const T& named_arg) - -> decltype(std::declval().map(named_arg.value)) { + -> decltype(this->map(named_arg.value)) { return map(named_arg.value); } @@ -1536,53 +1485,146 @@ enum { max_packed_args = 62 / packed_arg_bits }; enum : unsigned long long { is_unpacked_bit = 1ULL << 63 }; enum : unsigned long long { has_named_args_bit = 1ULL << 62 }; -FMT_END_DETAIL_NAMESPACE +template +auto copy_str(InputIt begin, InputIt end, appender out) -> appender { + get_container(out).append(begin, end); + return out; +} +template +auto copy_str(InputIt begin, InputIt end, + std::back_insert_iterator out) + -> std::back_insert_iterator { + get_container(out).append(begin, end); + return out; +} + +template +FMT_CONSTEXPR auto copy_str(R&& rng, OutputIt out) -> OutputIt { + return detail::copy_str(rng.begin(), rng.end(), out); +} -// An output iterator that appends to a buffer. -// It is used to reduce symbol sizes for the common case. -class appender : public std::back_insert_iterator> { - using base = std::back_insert_iterator>; +#if FMT_GCC_VERSION && FMT_GCC_VERSION < 500 +// A workaround for gcc 4.8 to make void_t work in a SFINAE context. +template struct void_t_impl { using type = void; }; +template using void_t = typename void_t_impl::type; +#else +template using void_t = void; +#endif - template - friend auto get_buffer(appender out) -> detail::buffer& { - return detail::get_container(out); - } +template +struct is_output_iterator : std::false_type {}; - public: - using std::back_insert_iterator>::back_insert_iterator; - appender(base it) noexcept : base(it) {} - FMT_UNCHECKED_ITERATOR(appender); +template +struct is_output_iterator< + It, T, + void_t::iterator_category, + decltype(*std::declval() = std::declval())>> + : std::true_type {}; - auto operator++() noexcept -> appender& { return *this; } - auto operator++(int) noexcept -> appender { return *this; } -}; +template struct is_back_insert_iterator : std::false_type {}; +template +struct is_back_insert_iterator> + : std::true_type {}; -// A formatting argument. It is a trivially copyable/constructible type to -// allow storage in basic_memory_buffer. -template class basic_format_arg { +// A type-erased reference to an std::locale to avoid a heavy include. +class locale_ref { private: - detail::value value_; - detail::type type_; - - template - friend FMT_CONSTEXPR auto detail::make_arg(T&& value) - -> basic_format_arg; + const void* locale_; // A type-erased pointer to std::locale. - template - friend FMT_CONSTEXPR auto visit_format_arg(Visitor&& vis, - const basic_format_arg& arg) - -> decltype(vis(0)); + public: + constexpr FMT_INLINE locale_ref() : locale_(nullptr) {} + template explicit locale_ref(const Locale& loc); - friend class basic_format_args; - friend class dynamic_format_arg_store; + explicit operator bool() const noexcept { return locale_ != nullptr; } - using char_type = typename Context::char_type; + template auto get() const -> Locale; +}; - template - friend struct detail::arg_data; +template constexpr auto encode_types() -> unsigned long long { + return 0; +} - basic_format_arg(const detail::named_arg_info* args, size_t size) - : value_(args, size) {} +template +constexpr auto encode_types() -> unsigned long long { + return static_cast(mapped_type_constant::value) | + (encode_types() << packed_arg_bits); +} + +#if defined(__cpp_if_constexpr) +// This type is intentionally undefined, only used for errors +template struct type_is_unformattable_for; +#endif + +template +FMT_CONSTEXPR FMT_INLINE auto make_arg(T& val) -> value { + using arg_type = remove_cvref_t().map(val))>; + + constexpr bool formattable_char = + !std::is_same::value; + static_assert(formattable_char, "Mixing character types is disallowed."); + + // Formatting of arbitrary pointers is disallowed. If you want to format a + // pointer cast it to `void*` or `const void*`. In particular, this forbids + // formatting of `[const] volatile char*` printed as bool by iostreams. + constexpr bool formattable_pointer = + !std::is_same::value; + static_assert(formattable_pointer, + "Formatting of non-void pointers is disallowed."); + + constexpr bool formattable = !std::is_same::value; +#if defined(__cpp_if_constexpr) + if constexpr (!formattable) { + type_is_unformattable_for _; + } +#endif + static_assert( + formattable, + "Cannot format an argument. To make type T formattable provide a " + "formatter specialization: https://fmt.dev/latest/api.html#udt"); + return {arg_mapper().map(val)}; +} + +template +FMT_CONSTEXPR auto make_arg(T& val) -> basic_format_arg { + auto arg = basic_format_arg(); + arg.type_ = mapped_type_constant::value; + arg.value_ = make_arg(val); + return arg; +} + +template +FMT_CONSTEXPR inline auto make_arg(T& val) -> basic_format_arg { + return make_arg(val); +} +} // namespace detail +FMT_BEGIN_EXPORT + +// A formatting argument. It is a trivially copyable/constructible type to +// allow storage in basic_memory_buffer. +template class basic_format_arg { + private: + detail::value value_; + detail::type type_; + + template + friend FMT_CONSTEXPR auto detail::make_arg(T& value) + -> basic_format_arg; + + template + friend FMT_CONSTEXPR auto visit_format_arg(Visitor&& vis, + const basic_format_arg& arg) + -> decltype(vis(0)); + + friend class basic_format_args; + friend class dynamic_format_arg_store; + + using char_type = typename Context::char_type; + + template + friend struct detail::arg_data; + + basic_format_arg(const detail::named_arg_info* args, size_t size) + : value_(args, size) {} public: class handle { @@ -1619,6 +1661,8 @@ template class basic_format_arg { ``vis(value)`` will be called with the value of type ``double``. \endrst */ +// DEPRECATED! +FMT_EXPORT template FMT_CONSTEXPR FMT_INLINE auto visit_format_arg( Visitor&& vis, const basic_format_arg& arg) -> decltype(vis(0)) { @@ -1660,136 +1704,8 @@ FMT_CONSTEXPR FMT_INLINE auto visit_format_arg( return vis(monostate()); } -FMT_BEGIN_DETAIL_NAMESPACE - -template -auto copy_str(InputIt begin, InputIt end, appender out) -> appender { - get_container(out).append(begin, end); - return out; -} - -template -FMT_CONSTEXPR auto copy_str(R&& rng, OutputIt out) -> OutputIt { - return detail::copy_str(rng.begin(), rng.end(), out); -} - -#if FMT_GCC_VERSION && FMT_GCC_VERSION < 500 -// A workaround for gcc 4.8 to make void_t work in a SFINAE context. -template struct void_t_impl { using type = void; }; -template -using void_t = typename detail::void_t_impl::type; -#else -template using void_t = void; -#endif - -template -struct is_output_iterator : std::false_type {}; - -template -struct is_output_iterator< - It, T, - void_t::iterator_category, - decltype(*std::declval() = std::declval())>> - : std::true_type {}; - -template -struct is_back_insert_iterator : std::false_type {}; -template -struct is_back_insert_iterator> - : std::true_type {}; - -template -struct is_contiguous_back_insert_iterator : std::false_type {}; -template -struct is_contiguous_back_insert_iterator> - : is_contiguous {}; -template <> -struct is_contiguous_back_insert_iterator : std::true_type {}; - -// A type-erased reference to an std::locale to avoid a heavy include. -class locale_ref { - private: - const void* locale_; // A type-erased pointer to std::locale. - - public: - constexpr locale_ref() : locale_(nullptr) {} - template explicit locale_ref(const Locale& loc); - - explicit operator bool() const noexcept { return locale_ != nullptr; } - - template auto get() const -> Locale; -}; - -template constexpr auto encode_types() -> unsigned long long { - return 0; -} - -template -constexpr auto encode_types() -> unsigned long long { - return static_cast(mapped_type_constant::value) | - (encode_types() << packed_arg_bits); -} - -template -FMT_CONSTEXPR FMT_INLINE auto make_value(T&& val) -> value { - const auto& arg = arg_mapper().map(FMT_FORWARD(val)); - - constexpr bool formattable_char = - !std::is_same::value; - static_assert(formattable_char, "Mixing character types is disallowed."); - - constexpr bool formattable_const = - !std::is_same::value; - static_assert(formattable_const, "Cannot format a const argument."); - - // Formatting of arbitrary pointers is disallowed. If you want to output - // a pointer cast it to "void *" or "const void *". In particular, this - // forbids formatting of "[const] volatile char *" which is printed as bool - // by iostreams. - constexpr bool formattable_pointer = - !std::is_same::value; - static_assert(formattable_pointer, - "Formatting of non-void pointers is disallowed."); - - constexpr bool formattable = - !std::is_same::value; - static_assert( - formattable, - "Cannot format an argument. To make type T formattable provide a " - "formatter specialization: https://fmt.dev/latest/api.html#udt"); - return {arg}; -} - -template -FMT_CONSTEXPR auto make_arg(T&& value) -> basic_format_arg { - basic_format_arg arg; - arg.type_ = mapped_type_constant::value; - arg.value_ = make_value(value); - return arg; -} - -// The type template parameter is there to avoid an ODR violation when using -// a fallback formatter in one translation unit and an implicit conversion in -// another (not recommended). -template -FMT_CONSTEXPR FMT_INLINE auto make_arg(T&& val) -> value { - return make_value(val); -} - -template -FMT_CONSTEXPR inline auto make_arg(T&& value) -> basic_format_arg { - return make_arg(value); -} -FMT_END_DETAIL_NAMESPACE - // Formatting context. template class basic_format_context { - public: - /** The character type for the output. */ - using char_type = Char; - private: OutputIt out_; basic_format_args args_; @@ -1798,31 +1714,32 @@ template class basic_format_context { public: using iterator = OutputIt; using format_arg = basic_format_arg; + using format_args = basic_format_args; using parse_context_type = basic_format_parse_context; - template using formatter_type = formatter; + template using formatter_type = formatter; + + /** The character type for the output. */ + using char_type = Char; basic_format_context(basic_format_context&&) = default; basic_format_context(const basic_format_context&) = delete; void operator=(const basic_format_context&) = delete; /** - Constructs a ``basic_format_context`` object. References to the arguments are - stored in the object so make sure they have appropriate lifetimes. + Constructs a ``basic_format_context`` object. References to the arguments + are stored in the object so make sure they have appropriate lifetimes. */ - constexpr basic_format_context( - OutputIt out, basic_format_args ctx_args, - detail::locale_ref loc = detail::locale_ref()) + constexpr basic_format_context(OutputIt out, format_args ctx_args, + detail::locale_ref loc = {}) : out_(out), args_(ctx_args), loc_(loc) {} constexpr auto arg(int id) const -> format_arg { return args_.get(id); } - FMT_CONSTEXPR auto arg(basic_string_view name) -> format_arg { + FMT_CONSTEXPR auto arg(basic_string_view name) -> format_arg { return args_.get(name); } - FMT_CONSTEXPR auto arg_id(basic_string_view name) -> int { + FMT_CONSTEXPR auto arg_id(basic_string_view name) -> int { return args_.get_id(name); } - auto args() const -> const basic_format_args& { - return args_; - } + auto args() const -> const format_args& { return args_; } FMT_CONSTEXPR auto error_handler() -> detail::error_handler { return {}; } void on_error(const char* message) { error_handler().on_error(message); } @@ -1843,16 +1760,10 @@ using buffer_context = basic_format_context, Char>; using format_context = buffer_context; -// Workaround an alias issue: https://stackoverflow.com/q/62767544/471164. -#define FMT_BUFFER_CONTEXT(Char) \ - basic_format_context, Char> - template -using is_formattable = bool_constant< - !std::is_base_of>().map( - std::declval()))>::value && - !detail::has_fallback_formatter::value>; +using is_formattable = bool_constant>() + .map(std::declval()))>::value>; /** \rst @@ -1870,7 +1781,7 @@ class format_arg_store { private: static const size_t num_args = sizeof...(Args); - static const size_t num_named_args = detail::count_named_args(); + static constexpr size_t num_named_args = detail::count_named_args(); static const bool is_packed = num_args <= detail::max_packed_args; using value_type = conditional_t, @@ -1891,16 +1802,14 @@ class format_arg_store public: template - FMT_CONSTEXPR FMT_INLINE format_arg_store(T&&... args) + FMT_CONSTEXPR FMT_INLINE format_arg_store(T&... args) : #if FMT_GCC_VERSION && FMT_GCC_VERSION < 409 basic_format_args(*this), #endif - data_{detail::make_arg< - is_packed, Context, - detail::mapped_type_constant, Context>::value>( - FMT_FORWARD(args))...} { - detail::init_named_args(data_.named_args(), 0, 0, args...); + data_{detail::make_arg(args)...} { + if (detail::const_check(num_named_args != 0)) + detail::init_named_args(data_.named_args(), 0, 0, args...); } }; @@ -1908,14 +1817,15 @@ class format_arg_store \rst Constructs a `~fmt::format_arg_store` object that contains references to arguments and can be implicitly converted to `~fmt::format_args`. `Context` - can be omitted in which case it defaults to `~fmt::context`. + can be omitted in which case it defaults to `~fmt::format_context`. See `~fmt::arg` for lifetime considerations. \endrst */ -template -constexpr auto make_format_args(Args&&... args) - -> format_arg_store...> { - return {FMT_FORWARD(args)...}; +// Arguments are taken by lvalue references to avoid some lifetime issues. +template +constexpr auto make_format_args(T&... args) + -> format_arg_store...> { + return {args...}; } /** @@ -1934,6 +1844,7 @@ inline auto arg(const Char* name, const T& arg) -> detail::named_arg { static_assert(!detail::is_named_arg(), "nested named arguments"); return {name, arg}; } +FMT_END_EXPORT /** \rst @@ -1942,7 +1853,7 @@ inline auto arg(const Char* name, const T& arg) -> detail::named_arg { ``vformat``:: void vlog(string_view format_str, format_args args); // OK - format_args args = make_format_args(42); // Error: dangling reference + format_args args = make_format_args(); // Error: dangling reference \endrst */ template class basic_format_args { @@ -2059,7 +1970,7 @@ template class basic_format_args { /** An alias to ``basic_format_args``. */ // A separate type would result in shorter symbols but break ABI compatibility // between clang and gcc on ARM (#1919). -using format_args = basic_format_args; +FMT_EXPORT using format_args = basic_format_args; // We cannot use enum classes as bit fields because of a gcc bug, so we put them // in namespaces instead (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61414). @@ -2080,7 +1991,7 @@ enum type FMT_ENUM_UNDERLYING_TYPE(unsigned char){none, minus, plus, space}; } using sign_t = sign::type; -FMT_BEGIN_DETAIL_NAMESPACE +namespace detail { // Workaround an array initialization issue in gcc 4.8. template struct fill_t { @@ -2092,7 +2003,7 @@ template struct fill_t { public: FMT_CONSTEXPR void operator=(basic_string_view s) { auto size = s.size(); - if (size > max_size) return throw_format_error("invalid fill"); + FMT_ASSERT(size <= max_size, "invalid fill"); for (size_t i = 0; i < size; ++i) data_[i] = s[i]; size_ = static_cast(size); } @@ -2105,11 +2016,10 @@ template struct fill_t { return data_[index]; } }; -FMT_END_DETAIL_NAMESPACE +} // namespace detail enum class presentation_type : unsigned char { none, - // Integer types should go first, dec, // 'd' oct, // 'o' hex_lower, // 'x' @@ -2131,7 +2041,7 @@ enum class presentation_type : unsigned char { }; // Format specifiers for built-in and string types. -template struct basic_format_specs { +template struct format_specs { int width; int precision; presentation_type type; @@ -2141,7 +2051,7 @@ template struct basic_format_specs { bool localized : 1; detail::fill_t fill; - constexpr basic_format_specs() + constexpr format_specs() : width(0), precision(-1), type(presentation_type::none), @@ -2151,9 +2061,7 @@ template struct basic_format_specs { localized(false) {} }; -using format_specs = basic_format_specs; - -FMT_BEGIN_DETAIL_NAMESPACE +namespace detail { enum class arg_id_kind { none, index, name }; @@ -2174,7 +2082,7 @@ template struct arg_ref { arg_id_kind kind; union value { - FMT_CONSTEXPR value(int id = 0) : index{id} {} + FMT_CONSTEXPR value(int idx = 0) : index(idx) {} FMT_CONSTEXPR value(basic_string_view n) : name(n) {} int index; @@ -2183,134 +2091,30 @@ template struct arg_ref { }; // Format specifiers with width and precision resolved at formatting rather -// than parsing time to allow re-using the same parsed specifiers with +// than parsing time to allow reusing the same parsed specifiers with // different sets of arguments (precompilation of format strings). -template -struct dynamic_format_specs : basic_format_specs { +template +struct dynamic_format_specs : format_specs { arg_ref width_ref; arg_ref precision_ref; }; -struct auto_id {}; - -// A format specifier handler that sets fields in basic_format_specs. -template class specs_setter { - protected: - basic_format_specs& specs_; - - public: - explicit FMT_CONSTEXPR specs_setter(basic_format_specs& specs) - : specs_(specs) {} - - FMT_CONSTEXPR specs_setter(const specs_setter& other) - : specs_(other.specs_) {} - - FMT_CONSTEXPR void on_align(align_t align) { specs_.align = align; } - FMT_CONSTEXPR void on_fill(basic_string_view fill) { - specs_.fill = fill; - } - FMT_CONSTEXPR void on_sign(sign_t s) { specs_.sign = s; } - FMT_CONSTEXPR void on_hash() { specs_.alt = true; } - FMT_CONSTEXPR void on_localized() { specs_.localized = true; } - - FMT_CONSTEXPR void on_zero() { - if (specs_.align == align::none) specs_.align = align::numeric; - specs_.fill[0] = Char('0'); - } - - FMT_CONSTEXPR void on_width(int width) { specs_.width = width; } - FMT_CONSTEXPR void on_precision(int precision) { - specs_.precision = precision; - } - FMT_CONSTEXPR void end_precision() {} - - FMT_CONSTEXPR void on_type(presentation_type type) { specs_.type = type; } -}; - -// Format spec handler that saves references to arguments representing dynamic -// width and precision to be resolved at formatting time. -template -class dynamic_specs_handler - : public specs_setter { - public: - using char_type = typename ParseContext::char_type; - - FMT_CONSTEXPR dynamic_specs_handler(dynamic_format_specs& specs, - ParseContext& ctx) - : specs_setter(specs), specs_(specs), context_(ctx) {} - - FMT_CONSTEXPR dynamic_specs_handler(const dynamic_specs_handler& other) - : specs_setter(other), - specs_(other.specs_), - context_(other.context_) {} - - template FMT_CONSTEXPR void on_dynamic_width(Id arg_id) { - specs_.width_ref = make_arg_ref(arg_id); - } - - template FMT_CONSTEXPR void on_dynamic_precision(Id arg_id) { - specs_.precision_ref = make_arg_ref(arg_id); - } - - FMT_CONSTEXPR void on_error(const char* message) { - context_.on_error(message); - } - - private: - dynamic_format_specs& specs_; - ParseContext& context_; - - using arg_ref_type = arg_ref; - - FMT_CONSTEXPR auto make_arg_ref(int arg_id) -> arg_ref_type { - context_.check_arg_id(arg_id); - context_.check_dynamic_spec(arg_id); - return arg_ref_type(arg_id); - } - - FMT_CONSTEXPR auto make_arg_ref(auto_id) -> arg_ref_type { - int arg_id = context_.next_arg_id(); - context_.check_dynamic_spec(arg_id); - return arg_ref_type(arg_id); - } - - FMT_CONSTEXPR auto make_arg_ref(basic_string_view arg_id) - -> arg_ref_type { - context_.check_arg_id(arg_id); - basic_string_view format_str( - context_.begin(), to_unsigned(context_.end() - context_.begin())); - return arg_ref_type(arg_id); - } -}; - -template constexpr bool is_ascii_letter(Char c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); -} - -// Converts a character to ASCII. Returns a number > 127 on conversion failure. +// Converts a character to ASCII. Returns '\0' on conversion failure. template ::value)> -constexpr auto to_ascii(Char c) -> Char { - return c; +constexpr auto to_ascii(Char c) -> char { + return c <= 0xff ? static_cast(c) : '\0'; } template ::value)> -constexpr auto to_ascii(Char c) -> underlying_t { - return c; -} - -FMT_CONSTEXPR inline auto code_point_length_impl(char c) -> int { - return "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0\0\0\2\2\2\2\3\3\4" - [static_cast(c) >> 3]; +constexpr auto to_ascii(Char c) -> char { + return c <= 0xff ? static_cast(c) : '\0'; } +// Returns the number of code units in a code point or 1 on error. template FMT_CONSTEXPR auto code_point_length(const Char* begin) -> int { if (const_check(sizeof(Char) != 1)) return 1; - int len = code_point_length_impl(static_cast(*begin)); - - // Compute the pointer to the next character early so that the next - // iteration can start working on the next character. Neither Clang - // nor GCC figure out this reordering on their own. - return len + !len; + auto c = static_cast(*begin); + return static_cast((0x3a55000000000000ull >> (2 * (c >> 3))) & 0x3) + 1; } // Return the result via the out param to workaround gcc bug 77539. @@ -2355,279 +2159,284 @@ FMT_CONSTEXPR auto parse_nonnegative_int(const Char*& begin, const Char* end, : error_value; } -// Parses fill and alignment. -template -FMT_CONSTEXPR auto parse_align(const Char* begin, const Char* end, - Handler&& handler) -> const Char* { - FMT_ASSERT(begin != end, ""); - auto align = align::none; - auto p = begin + code_point_length(begin); - if (end - p <= 0) p = begin; - for (;;) { - switch (to_ascii(*p)) { - case '<': - align = align::left; - break; - case '>': - align = align::right; - break; - case '^': - align = align::center; - break; - default: - break; - } - if (align != align::none) { - if (p != begin) { - auto c = *begin; - if (c == '{') - return handler.on_error("invalid fill character '{'"), begin; - handler.on_fill(basic_string_view(begin, to_unsigned(p - begin))); - begin = p + 1; - } else - ++begin; - handler.on_align(align); - break; - } else if (p == begin) { - break; - } - p = begin; +FMT_CONSTEXPR inline auto parse_align(char c) -> align_t { + switch (c) { + case '<': + return align::left; + case '>': + return align::right; + case '^': + return align::center; } - return begin; + return align::none; } -template FMT_CONSTEXPR bool is_name_start(Char c) { - return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || '_' == c; +template constexpr auto is_name_start(Char c) -> bool { + return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '_'; } -template +template FMT_CONSTEXPR auto do_parse_arg_id(const Char* begin, const Char* end, - IDHandler&& handler) -> const Char* { - FMT_ASSERT(begin != end, ""); + Handler&& handler) -> const Char* { Char c = *begin; if (c >= '0' && c <= '9') { int index = 0; + constexpr int max = (std::numeric_limits::max)(); if (c != '0') - index = - parse_nonnegative_int(begin, end, (std::numeric_limits::max)()); + index = parse_nonnegative_int(begin, end, max); else ++begin; if (begin == end || (*begin != '}' && *begin != ':')) - handler.on_error("invalid format string"); + throw_format_error("invalid format string"); else - handler(index); + handler.on_index(index); return begin; } if (!is_name_start(c)) { - handler.on_error("invalid format string"); + throw_format_error("invalid format string"); return begin; } auto it = begin; do { ++it; - } while (it != end && (is_name_start(c = *it) || ('0' <= c && c <= '9'))); - handler(basic_string_view(begin, to_unsigned(it - begin))); + } while (it != end && (is_name_start(*it) || ('0' <= *it && *it <= '9'))); + handler.on_name({begin, to_unsigned(it - begin)}); return it; } -template +template FMT_CONSTEXPR FMT_INLINE auto parse_arg_id(const Char* begin, const Char* end, - IDHandler&& handler) -> const Char* { + Handler&& handler) -> const Char* { + FMT_ASSERT(begin != end, ""); Char c = *begin; if (c != '}' && c != ':') return do_parse_arg_id(begin, end, handler); - handler(); + handler.on_auto(); return begin; } -template -FMT_CONSTEXPR auto parse_width(const Char* begin, const Char* end, - Handler&& handler) -> const Char* { - using detail::auto_id; - struct width_adapter { - Handler& handler; +template struct dynamic_spec_id_handler { + basic_format_parse_context& ctx; + arg_ref& ref; - FMT_CONSTEXPR void operator()() { handler.on_dynamic_width(auto_id()); } - FMT_CONSTEXPR void operator()(int id) { handler.on_dynamic_width(id); } - FMT_CONSTEXPR void operator()(basic_string_view id) { - handler.on_dynamic_width(id); - } - FMT_CONSTEXPR void on_error(const char* message) { - if (message) handler.on_error(message); - } - }; + FMT_CONSTEXPR void on_auto() { + int id = ctx.next_arg_id(); + ref = arg_ref(id); + ctx.check_dynamic_spec(id); + } + FMT_CONSTEXPR void on_index(int id) { + ref = arg_ref(id); + ctx.check_arg_id(id); + ctx.check_dynamic_spec(id); + } + FMT_CONSTEXPR void on_name(basic_string_view id) { + ref = arg_ref(id); + ctx.check_arg_id(id); + } +}; +// Parses [integer | "{" [arg_id] "}"]. +template +FMT_CONSTEXPR auto parse_dynamic_spec(const Char* begin, const Char* end, + int& value, arg_ref& ref, + basic_format_parse_context& ctx) + -> const Char* { FMT_ASSERT(begin != end, ""); if ('0' <= *begin && *begin <= '9') { - int width = parse_nonnegative_int(begin, end, -1); - if (width != -1) - handler.on_width(width); + int val = parse_nonnegative_int(begin, end, -1); + if (val != -1) + value = val; else - handler.on_error("number is too big"); + throw_format_error("number is too big"); } else if (*begin == '{') { ++begin; - if (begin != end) begin = parse_arg_id(begin, end, width_adapter{handler}); - if (begin == end || *begin != '}') - return handler.on_error("invalid format string"), begin; - ++begin; - } - return begin; -} - -template -FMT_CONSTEXPR auto parse_precision(const Char* begin, const Char* end, - Handler&& handler) -> const Char* { - using detail::auto_id; - struct precision_adapter { - Handler& handler; - - FMT_CONSTEXPR void operator()() { handler.on_dynamic_precision(auto_id()); } - FMT_CONSTEXPR void operator()(int id) { handler.on_dynamic_precision(id); } - FMT_CONSTEXPR void operator()(basic_string_view id) { - handler.on_dynamic_precision(id); - } - FMT_CONSTEXPR void on_error(const char* message) { - if (message) handler.on_error(message); - } - }; - - ++begin; - auto c = begin != end ? *begin : Char(); - if ('0' <= c && c <= '9') { - auto precision = parse_nonnegative_int(begin, end, -1); - if (precision != -1) - handler.on_precision(precision); - else - handler.on_error("number is too big"); - } else if (c == '{') { - ++begin; - if (begin != end) - begin = parse_arg_id(begin, end, precision_adapter{handler}); - if (begin == end || *begin++ != '}') - return handler.on_error("invalid format string"), begin; - } else { - return handler.on_error("missing precision specifier"), begin; + auto handler = dynamic_spec_id_handler{ctx, ref}; + if (begin != end) begin = parse_arg_id(begin, end, handler); + if (begin != end && *begin == '}') return ++begin; + throw_format_error("invalid format string"); } - handler.end_precision(); return begin; } template -FMT_CONSTEXPR auto parse_presentation_type(Char type) -> presentation_type { - switch (to_ascii(type)) { - case 'd': - return presentation_type::dec; - case 'o': - return presentation_type::oct; - case 'x': - return presentation_type::hex_lower; - case 'X': - return presentation_type::hex_upper; - case 'b': - return presentation_type::bin_lower; - case 'B': - return presentation_type::bin_upper; - case 'a': - return presentation_type::hexfloat_lower; - case 'A': - return presentation_type::hexfloat_upper; - case 'e': - return presentation_type::exp_lower; - case 'E': - return presentation_type::exp_upper; - case 'f': - return presentation_type::fixed_lower; - case 'F': - return presentation_type::fixed_upper; - case 'g': - return presentation_type::general_lower; - case 'G': - return presentation_type::general_upper; - case 'c': - return presentation_type::chr; - case 's': - return presentation_type::string; - case 'p': - return presentation_type::pointer; - case '?': - return presentation_type::debug; - default: - return presentation_type::none; - } -} - -// Parses standard format specifiers and sends notifications about parsed -// components to handler. -template -FMT_CONSTEXPR FMT_INLINE auto parse_format_specs(const Char* begin, - const Char* end, - SpecHandler&& handler) +FMT_CONSTEXPR auto parse_precision(const Char* begin, const Char* end, + int& value, arg_ref& ref, + basic_format_parse_context& ctx) -> const Char* { - if (1 < end - begin && begin[1] == '}' && is_ascii_letter(*begin) && - *begin != 'L') { - presentation_type type = parse_presentation_type(*begin++); - if (type == presentation_type::none) - handler.on_error("invalid type specifier"); - handler.on_type(type); + ++begin; + if (begin == end || *begin == '}') { + throw_format_error("invalid precision"); return begin; } + return parse_dynamic_spec(begin, end, value, ref, ctx); +} - if (begin == end) return begin; - - begin = parse_align(begin, end, handler); - if (begin == end) return begin; - - // Parse sign. - switch (to_ascii(*begin)) { - case '+': - handler.on_sign(sign::plus); - ++begin; - break; - case '-': - handler.on_sign(sign::minus); - ++begin; - break; - case ' ': - handler.on_sign(sign::space); - ++begin; - break; - default: - break; - } - if (begin == end) return begin; - - if (*begin == '#') { - handler.on_hash(); - if (++begin == end) return begin; - } - - // Parse zero flag. - if (*begin == '0') { - handler.on_zero(); - if (++begin == end) return begin; - } - - begin = parse_width(begin, end, handler); - if (begin == end) return begin; +enum class state { start, align, sign, hash, zero, width, precision, locale }; - // Parse precision. - if (*begin == '.') { - begin = parse_precision(begin, end, handler); +// Parses standard format specifiers. +template +FMT_CONSTEXPR FMT_INLINE auto parse_format_specs( + const Char* begin, const Char* end, dynamic_format_specs& specs, + basic_format_parse_context& ctx, type arg_type) -> const Char* { + auto c = '\0'; + if (end - begin > 1) { + auto next = to_ascii(begin[1]); + c = parse_align(next) == align::none ? to_ascii(*begin) : '\0'; + } else { if (begin == end) return begin; + c = to_ascii(*begin); } - if (*begin == 'L') { - handler.on_localized(); - ++begin; - } + struct { + state current_state = state::start; + FMT_CONSTEXPR void operator()(state s, bool valid = true) { + if (current_state >= s || !valid) + throw_format_error("invalid format specifier"); + current_state = s; + } + } enter_state; + + using pres = presentation_type; + constexpr auto integral_set = sint_set | uint_set | bool_set | char_set; + struct { + const Char*& begin; + dynamic_format_specs& specs; + type arg_type; + + FMT_CONSTEXPR auto operator()(pres type, int set) -> const Char* { + if (!in(arg_type, set)) throw_format_error("invalid format specifier"); + specs.type = type; + return begin + 1; + } + } parse_presentation_type{begin, specs, arg_type}; - // Parse type. - if (begin != end && *begin != '}') { - presentation_type type = parse_presentation_type(*begin++); - if (type == presentation_type::none) - handler.on_error("invalid type specifier"); - handler.on_type(type); + for (;;) { + switch (c) { + case '<': + case '>': + case '^': + enter_state(state::align); + specs.align = parse_align(c); + ++begin; + break; + case '+': + case '-': + case ' ': + enter_state(state::sign, in(arg_type, sint_set | float_set)); + switch (c) { + case '+': + specs.sign = sign::plus; + break; + case '-': + specs.sign = sign::minus; + break; + case ' ': + specs.sign = sign::space; + break; + } + ++begin; + break; + case '#': + enter_state(state::hash, is_arithmetic_type(arg_type)); + specs.alt = true; + ++begin; + break; + case '0': + enter_state(state::zero); + if (!is_arithmetic_type(arg_type)) + throw_format_error("format specifier requires numeric argument"); + if (specs.align == align::none) { + // Ignore 0 if align is specified for compatibility with std::format. + specs.align = align::numeric; + specs.fill[0] = Char('0'); + } + ++begin; + break; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '{': + enter_state(state::width); + begin = parse_dynamic_spec(begin, end, specs.width, specs.width_ref, ctx); + break; + case '.': + enter_state(state::precision, + in(arg_type, float_set | string_set | cstring_set)); + begin = parse_precision(begin, end, specs.precision, specs.precision_ref, + ctx); + break; + case 'L': + enter_state(state::locale, is_arithmetic_type(arg_type)); + specs.localized = true; + ++begin; + break; + case 'd': + return parse_presentation_type(pres::dec, integral_set); + case 'o': + return parse_presentation_type(pres::oct, integral_set); + case 'x': + return parse_presentation_type(pres::hex_lower, integral_set); + case 'X': + return parse_presentation_type(pres::hex_upper, integral_set); + case 'b': + return parse_presentation_type(pres::bin_lower, integral_set); + case 'B': + return parse_presentation_type(pres::bin_upper, integral_set); + case 'a': + return parse_presentation_type(pres::hexfloat_lower, float_set); + case 'A': + return parse_presentation_type(pres::hexfloat_upper, float_set); + case 'e': + return parse_presentation_type(pres::exp_lower, float_set); + case 'E': + return parse_presentation_type(pres::exp_upper, float_set); + case 'f': + return parse_presentation_type(pres::fixed_lower, float_set); + case 'F': + return parse_presentation_type(pres::fixed_upper, float_set); + case 'g': + return parse_presentation_type(pres::general_lower, float_set); + case 'G': + return parse_presentation_type(pres::general_upper, float_set); + case 'c': + return parse_presentation_type(pres::chr, integral_set); + case 's': + return parse_presentation_type(pres::string, + bool_set | string_set | cstring_set); + case 'p': + return parse_presentation_type(pres::pointer, pointer_set | cstring_set); + case '?': + return parse_presentation_type(pres::debug, + char_set | string_set | cstring_set); + case '}': + return begin; + default: { + if (*begin == '}') return begin; + // Parse fill and alignment. + auto fill_end = begin + code_point_length(begin); + if (end - fill_end <= 0) { + throw_format_error("invalid format specifier"); + return begin; + } + if (*begin == '{') { + throw_format_error("invalid fill character '{'"); + return begin; + } + auto align = parse_align(to_ascii(*fill_end)); + enter_state(state::align, align != align::none); + specs.fill = {begin, to_unsigned(fill_end - begin)}; + specs.align = align; + begin = fill_end + 1; + } + } + if (begin == end) return begin; + c = to_ascii(*begin); } - return begin; } template @@ -2637,14 +2446,11 @@ FMT_CONSTEXPR auto parse_replacement_field(const Char* begin, const Char* end, Handler& handler; int arg_id; - FMT_CONSTEXPR void operator()() { arg_id = handler.on_arg_id(); } - FMT_CONSTEXPR void operator()(int id) { arg_id = handler.on_arg_id(id); } - FMT_CONSTEXPR void operator()(basic_string_view id) { + FMT_CONSTEXPR void on_auto() { arg_id = handler.on_arg_id(); } + FMT_CONSTEXPR void on_index(int id) { arg_id = handler.on_arg_id(id); } + FMT_CONSTEXPR void on_name(basic_string_view id) { arg_id = handler.on_arg_id(id); } - FMT_CONSTEXPR void on_error(const char* message) { - if (message) handler.on_error(message); - } }; ++begin; @@ -2673,9 +2479,6 @@ FMT_CONSTEXPR auto parse_replacement_field(const Char* begin, const Char* end, template FMT_CONSTEXPR FMT_INLINE void parse_format_string( basic_string_view format_str, Handler&& handler) { - // Workaround a name-lookup bug in MSVC's modules implementation. - using detail::find; - auto begin = format_str.data(); auto end = begin + format_str.size(); if (end - begin < 32) { @@ -2735,189 +2538,46 @@ FMT_CONSTEXPR auto parse_format_specs(ParseContext& ctx) -> decltype(ctx.begin()) { using char_type = typename ParseContext::char_type; using context = buffer_context; - using stripped_type = typename strip_named_arg::type; using mapped_type = conditional_t< mapped_type_constant::value != type::custom_type, decltype(arg_mapper().map(std::declval())), - stripped_type>; - auto f = conditional_t::value, - formatter, - fallback_formatter>(); - return f.parse(ctx); -} - -template -FMT_CONSTEXPR void check_int_type_spec(presentation_type type, - ErrorHandler&& eh) { - if (type > presentation_type::bin_upper && type != presentation_type::chr) - eh.on_error("invalid type specifier"); + typename strip_named_arg::type>; +#if defined(__cpp_if_constexpr) + if constexpr (std::is_default_constructible_v< + formatter>) { + return formatter().parse(ctx); + } else { + type_is_unformattable_for _; + return ctx.begin(); + } +#else + return formatter().parse(ctx); +#endif } -// Checks char specs and returns true if the type spec is char (and not int). -template -FMT_CONSTEXPR auto check_char_specs(const basic_format_specs& specs, - ErrorHandler&& eh = {}) -> bool { +// Checks char specs and returns true iff the presentation type is char-like. +template +FMT_CONSTEXPR auto check_char_specs(const format_specs& specs) -> bool { if (specs.type != presentation_type::none && specs.type != presentation_type::chr && specs.type != presentation_type::debug) { - check_int_type_spec(specs.type, eh); return false; } if (specs.align == align::numeric || specs.sign != sign::none || specs.alt) - eh.on_error("invalid format specifier for char"); + throw_format_error("invalid format specifier for char"); return true; } -// A floating-point presentation format. -enum class float_format : unsigned char { - general, // General: exponent notation or fixed point based on magnitude. - exp, // Exponent notation with the default precision of 6, e.g. 1.2e-3. - fixed, // Fixed point with the default precision of 6, e.g. 0.0012. - hex -}; - -struct float_specs { - int precision; - float_format format : 8; - sign_t sign : 8; - bool upper : 1; - bool locale : 1; - bool binary32 : 1; - bool showpoint : 1; -}; - -template -FMT_CONSTEXPR auto parse_float_type_spec(const basic_format_specs& specs, - ErrorHandler&& eh = {}) - -> float_specs { - auto result = float_specs(); - result.showpoint = specs.alt; - result.locale = specs.localized; - switch (specs.type) { - case presentation_type::none: - result.format = float_format::general; - break; - case presentation_type::general_upper: - result.upper = true; - FMT_FALLTHROUGH; - case presentation_type::general_lower: - result.format = float_format::general; - break; - case presentation_type::exp_upper: - result.upper = true; - FMT_FALLTHROUGH; - case presentation_type::exp_lower: - result.format = float_format::exp; - result.showpoint |= specs.precision != 0; - break; - case presentation_type::fixed_upper: - result.upper = true; - FMT_FALLTHROUGH; - case presentation_type::fixed_lower: - result.format = float_format::fixed; - result.showpoint |= specs.precision != 0; - break; - case presentation_type::hexfloat_upper: - result.upper = true; - FMT_FALLTHROUGH; - case presentation_type::hexfloat_lower: - result.format = float_format::hex; - break; - default: - eh.on_error("invalid type specifier"); - break; - } - return result; -} - -template -FMT_CONSTEXPR auto check_cstring_type_spec(presentation_type type, - ErrorHandler&& eh = {}) -> bool { - if (type == presentation_type::none || type == presentation_type::string || - type == presentation_type::debug) - return true; - if (type != presentation_type::pointer) eh.on_error("invalid type specifier"); - return false; -} - -template -FMT_CONSTEXPR void check_string_type_spec(presentation_type type, - ErrorHandler&& eh = {}) { - if (type != presentation_type::none && type != presentation_type::string && - type != presentation_type::debug) - eh.on_error("invalid type specifier"); -} - -template -FMT_CONSTEXPR void check_pointer_type_spec(presentation_type type, - ErrorHandler&& eh) { - if (type != presentation_type::none && type != presentation_type::pointer) - eh.on_error("invalid type specifier"); -} - -// A parse_format_specs handler that checks if specifiers are consistent with -// the argument type. -template class specs_checker : public Handler { - private: - detail::type arg_type_; - - FMT_CONSTEXPR void require_numeric_argument() { - if (!is_arithmetic_type(arg_type_)) - this->on_error("format specifier requires numeric argument"); - } - - public: - FMT_CONSTEXPR specs_checker(const Handler& handler, detail::type arg_type) - : Handler(handler), arg_type_(arg_type) {} - - FMT_CONSTEXPR void on_align(align_t align) { - if (align == align::numeric) require_numeric_argument(); - Handler::on_align(align); - } - - FMT_CONSTEXPR void on_sign(sign_t s) { - require_numeric_argument(); - if (is_integral_type(arg_type_) && arg_type_ != type::int_type && - arg_type_ != type::long_long_type && arg_type_ != type::int128_type && - arg_type_ != type::char_type) { - this->on_error("format specifier requires signed argument"); - } - Handler::on_sign(s); - } - - FMT_CONSTEXPR void on_hash() { - require_numeric_argument(); - Handler::on_hash(); - } - - FMT_CONSTEXPR void on_localized() { - require_numeric_argument(); - Handler::on_localized(); - } - - FMT_CONSTEXPR void on_zero() { - require_numeric_argument(); - Handler::on_zero(); - } - - FMT_CONSTEXPR void end_precision() { - if (is_integral_type(arg_type_) || arg_type_ == type::pointer_type) - this->on_error("precision not allowed for this argument type"); - } -}; - -constexpr int invalid_arg_index = -1; - #if FMT_USE_NONTYPE_TEMPLATE_ARGS template constexpr auto get_arg_index_by_name(basic_string_view name) -> int { - if constexpr (detail::is_statically_named_arg()) { + if constexpr (is_statically_named_arg()) { if (name == T::name) return N; } if constexpr (sizeof...(Args) > 0) return get_arg_index_by_name(name); (void)name; // Workaround an MSVC bug about "unused" parameter. - return invalid_arg_index; + return -1; } #endif @@ -2928,34 +2588,29 @@ FMT_CONSTEXPR auto get_arg_index_by_name(basic_string_view name) -> int { return get_arg_index_by_name<0, Args...>(name); #endif (void)name; - return invalid_arg_index; + return -1; } -template -class format_string_checker { +template class format_string_checker { private: - // In the future basic_format_parse_context will replace compile_parse_context - // here and will use is_constant_evaluated and downcasting to access the data - // needed for compile-time checks: https://godbolt.org/z/GvWzcTjh1. - using parse_context_type = compile_parse_context; + using parse_context_type = compile_parse_context; static constexpr int num_args = sizeof...(Args); // Format specifier parsing function. + // In the future basic_format_parse_context will replace compile_parse_context + // here and will use is_constant_evaluated and downcasting to access the data + // needed for compile-time checks: https://godbolt.org/z/GvWzcTjh1. using parse_func = const Char* (*)(parse_context_type&); + type types_[num_args > 0 ? static_cast(num_args) : 1]; parse_context_type context_; parse_func parse_funcs_[num_args > 0 ? static_cast(num_args) : 1]; - type types_[num_args > 0 ? static_cast(num_args) : 1]; public: - explicit FMT_CONSTEXPR format_string_checker( - basic_string_view format_str, ErrorHandler eh) - : context_(format_str, num_args, types_, eh), - parse_funcs_{&parse_format_specs...}, - types_{ - mapped_type_constant>::value...} { - } + explicit FMT_CONSTEXPR format_string_checker(basic_string_view fmt) + : types_{mapped_type_constant>::value...}, + context_(fmt, num_args, types_), + parse_funcs_{&parse_format_specs...} {} FMT_CONSTEXPR void on_text(const Char*, const Char*) {} @@ -2966,8 +2621,8 @@ class format_string_checker { FMT_CONSTEXPR auto on_arg_id(basic_string_view id) -> int { #if FMT_USE_NONTYPE_TEMPLATE_ARGS auto index = get_arg_index_by_name(id); - if (index == invalid_arg_index) on_error("named argument is not found"); - return context_.check_arg_id(index), index; + if (index < 0) on_error("named argument is not found"); + return index; #else (void)id; on_error("compile-time checks for named arguments require C++20 support"); @@ -2975,17 +2630,19 @@ class format_string_checker { #endif } - FMT_CONSTEXPR void on_replacement_field(int, const Char*) {} + FMT_CONSTEXPR void on_replacement_field(int id, const Char* begin) { + on_format_specs(id, begin, begin); // Call parse() on empty specs. + } FMT_CONSTEXPR auto on_format_specs(int id, const Char* begin, const Char*) -> const Char* { - context_.advance_to(context_.begin() + (begin - &*context_.begin())); + context_.advance_to(begin); // id >= 0 check is a workaround for gcc 10 bug (#2065). return id >= 0 && id < num_args ? parse_funcs_[id](context_) : begin; } FMT_CONSTEXPR void on_error(const char* message) { - context_.on_error(message); + throw_format_error(message); } }; @@ -3001,28 +2658,33 @@ FMT_INLINE void check_format_string(const S&) { template ::value)> void check_format_string(S format_str) { - FMT_CONSTEXPR auto s = basic_string_view(format_str); - using checker = format_string_checker...>; - FMT_CONSTEXPR bool invalid_format = - (parse_format_string(s, checker(s, {})), true); - ignore_unused(invalid_format); + using char_t = typename S::char_type; + FMT_CONSTEXPR auto s = basic_string_view(format_str); + using checker = format_string_checker...>; + FMT_CONSTEXPR bool error = (parse_format_string(s, checker(s)), true); + ignore_unused(error); } +template struct vformat_args { + using type = basic_format_args< + basic_format_context>, Char>>; +}; +template <> struct vformat_args { using type = format_args; }; + +// Use vformat_args and avoid type_identity to keep symbols short. template -void vformat_to( - buffer& buf, basic_string_view fmt, - basic_format_args)> args, - locale_ref loc = {}); +void vformat_to(buffer& buf, basic_string_view fmt, + typename vformat_args::type args, locale_ref loc = {}); FMT_API void vprint_mojibake(std::FILE*, string_view, format_args); #ifndef _WIN32 inline void vprint_mojibake(std::FILE*, string_view, format_args) {} #endif -FMT_END_DETAIL_NAMESPACE +} // namespace detail + +FMT_BEGIN_EXPORT -// A formatter specialization for the core types corresponding to detail::type -// constants. +// A formatter specialization for natively supported types. template struct formatter::value != @@ -3031,81 +2693,21 @@ struct formatter specs_; public: - // Parses format specifiers stopping either at the end of the range or at the - // terminating '}'. template - FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { - auto begin = ctx.begin(), end = ctx.end(); - if (begin == end) return begin; - using handler_type = detail::dynamic_specs_handler; + FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const Char* { auto type = detail::type_constant::value; - auto checker = - detail::specs_checker(handler_type(specs_, ctx), type); - auto it = detail::parse_format_specs(begin, end, checker); - auto eh = ctx.error_handler(); - switch (type) { - case detail::type::none_type: - FMT_ASSERT(false, "invalid argument type"); - break; - case detail::type::bool_type: - if (specs_.type == presentation_type::none || - specs_.type == presentation_type::string) { - break; - } - FMT_FALLTHROUGH; - case detail::type::int_type: - case detail::type::uint_type: - case detail::type::long_long_type: - case detail::type::ulong_long_type: - case detail::type::int128_type: - case detail::type::uint128_type: - detail::check_int_type_spec(specs_.type, eh); - break; - case detail::type::char_type: - detail::check_char_specs(specs_, eh); - break; - case detail::type::float_type: - if (detail::const_check(FMT_USE_FLOAT)) - detail::parse_float_type_spec(specs_, eh); - else - FMT_ASSERT(false, "float support disabled"); - break; - case detail::type::double_type: - if (detail::const_check(FMT_USE_DOUBLE)) - detail::parse_float_type_spec(specs_, eh); - else - FMT_ASSERT(false, "double support disabled"); - break; - case detail::type::long_double_type: - if (detail::const_check(FMT_USE_LONG_DOUBLE)) - detail::parse_float_type_spec(specs_, eh); - else - FMT_ASSERT(false, "long double support disabled"); - break; - case detail::type::cstring_type: - detail::check_cstring_type_spec(specs_.type, eh); - break; - case detail::type::string_type: - detail::check_string_type_spec(specs_.type, eh); - break; - case detail::type::pointer_type: - detail::check_pointer_type_spec(specs_.type, eh); - break; - case detail::type::custom_type: - // Custom format specifiers are checked in parse functions of - // formatter specializations. - break; - } - return it; + auto end = + detail::parse_format_specs(ctx.begin(), ctx.end(), specs_, ctx, type); + if (type == detail::type::char_type) detail::check_char_specs(specs_); + return end; } template ::value, - enable_if_t<(U == detail::type::string_type || - U == detail::type::cstring_type || - U == detail::type::char_type), - int> = 0> - FMT_CONSTEXPR void set_debug_format() { - specs_.type = presentation_type::debug; + FMT_ENABLE_IF(U == detail::type::string_type || + U == detail::type::cstring_type || + U == detail::type::char_type)> + FMT_CONSTEXPR void set_debug_format(bool set = true) { + specs_.type = set ? presentation_type::debug : presentation_type::none; } template @@ -3113,28 +2715,9 @@ struct formatter decltype(ctx.out()); }; -#define FMT_FORMAT_AS(Type, Base) \ - template \ - struct formatter : formatter { \ - template \ - auto format(Type const& val, FormatContext& ctx) const \ - -> decltype(ctx.out()) { \ - return formatter::format(static_cast(val), ctx); \ - } \ - } - -FMT_FORMAT_AS(signed char, int); -FMT_FORMAT_AS(unsigned char, unsigned); -FMT_FORMAT_AS(short, int); -FMT_FORMAT_AS(unsigned short, unsigned); -FMT_FORMAT_AS(long, long long); -FMT_FORMAT_AS(unsigned long, unsigned long long); -FMT_FORMAT_AS(Char*, const Char*); -FMT_FORMAT_AS(std::basic_string, basic_string_view); -FMT_FORMAT_AS(std::nullptr_t, const void*); -FMT_FORMAT_AS(detail::std_string_view, basic_string_view); - -template struct basic_runtime { basic_string_view str; }; +template struct runtime_format_string { + basic_string_view str; +}; /** A compile-time format string. */ template class basic_format_string { @@ -3154,17 +2737,18 @@ template class basic_format_string { #ifdef FMT_HAS_CONSTEVAL if constexpr (detail::count_named_args() == detail::count_statically_named_args()) { - using checker = detail::format_string_checker...>; - detail::parse_format_string(str_, checker(s, {})); + using checker = + detail::format_string_checker...>; + detail::parse_format_string(str_, checker(s)); } #else detail::check_format_string(s); #endif } - basic_format_string(basic_runtime r) : str_(r.str) {} + basic_format_string(runtime_format_string fmt) : str_(fmt.str) {} FMT_INLINE operator basic_string_view() const { return str_; } + FMT_INLINE auto get() const -> basic_string_view { return str_; } }; #if FMT_GCC_VERSION && FMT_GCC_VERSION < 409 @@ -3184,7 +2768,7 @@ using format_string = basic_format_string...>; fmt::print(fmt::runtime("{:d}"), "I am not a number"); \endrst */ -inline auto runtime(string_view s) -> basic_runtime { return {{s}}; } +inline auto runtime(string_view s) -> runtime_format_string<> { return {{s}}; } #endif FMT_API auto vformat(string_view fmt, format_args args) -> std::string; @@ -3210,10 +2794,9 @@ FMT_NODISCARD FMT_INLINE auto format(format_string fmt, T&&... args) template ::value)> auto vformat_to(OutputIt out, string_view fmt, format_args args) -> OutputIt { - using detail::get_buffer; - auto&& buf = get_buffer(out); + auto&& buf = detail::get_buffer(out); detail::vformat_to(buf, fmt, args, {}); - return detail::get_iterator(buf); + return detail::get_iterator(buf, out); } /** @@ -3272,7 +2855,7 @@ template FMT_NODISCARD FMT_INLINE auto formatted_size(format_string fmt, T&&... args) -> size_t { auto buf = detail::counting_buffer<>(); - detail::vformat_to(buf, string_view(fmt), fmt::make_format_args(args...), {}); + detail::vformat_to(buf, fmt, fmt::make_format_args(args...), {}); return buf.count(); } @@ -3313,7 +2896,25 @@ FMT_INLINE void print(std::FILE* f, format_string fmt, T&&... args) { : detail::vprint_mojibake(f, fmt, vargs); } -FMT_MODULE_EXPORT_END +/** + Formats ``args`` according to specifications in ``fmt`` and writes the + output to the file ``f`` followed by a newline. + */ +template +FMT_INLINE void println(std::FILE* f, format_string fmt, T&&... args) { + return fmt::print(f, "{}\n", fmt::format(fmt, std::forward(args)...)); +} + +/** + Formats ``args`` according to specifications in ``fmt`` and writes the output + to ``stdout`` followed by a newline. + */ +template +FMT_INLINE void println(format_string fmt, T&&... args) { + return fmt::println(stdout, fmt, std::forward(args)...); +} + +FMT_END_EXPORT FMT_GCC_PRAGMA("GCC pop_options") FMT_END_NAMESPACE diff --git a/contrib/fmt/include/fmt/format-inl.h b/contrib/fmt/include/fmt/format-inl.h index 22b1ec8df..dac2d437a 100644 --- a/contrib/fmt/include/fmt/format-inl.h +++ b/contrib/fmt/include/fmt/format-inl.h @@ -9,13 +9,9 @@ #define FMT_FORMAT_INL_H_ #include -#include #include // errno #include #include -#include -#include // std::memmove -#include #include #ifndef FMT_STATIC_THOUSANDS_SEPARATOR @@ -115,16 +111,43 @@ template FMT_FUNC Char decimal_point_impl(locale_ref) { return '.'; } #endif + +FMT_FUNC auto write_loc(appender out, loc_value value, + const format_specs<>& specs, locale_ref loc) -> bool { +#ifndef FMT_STATIC_THOUSANDS_SEPARATOR + auto locale = loc.get(); + // We cannot use the num_put facet because it may produce output in + // a wrong encoding. + using facet = format_facet; + if (std::has_facet(locale)) + return std::use_facet(locale).put(out, value, specs); + return facet(locale).put(out, value, specs); +#endif + return false; +} } // namespace detail -#if !FMT_MSC_VERSION -FMT_API FMT_FUNC format_error::~format_error() noexcept = default; +template typename Locale::id format_facet::id; + +#ifndef FMT_STATIC_THOUSANDS_SEPARATOR +template format_facet::format_facet(Locale& loc) { + auto& numpunct = std::use_facet>(loc); + grouping_ = numpunct.grouping(); + if (!grouping_.empty()) separator_ = std::string(1, numpunct.thousands_sep()); +} + +template <> +FMT_API FMT_FUNC auto format_facet::do_put( + appender out, loc_value val, const format_specs<>& specs) const -> bool { + return val.visit( + detail::loc_writer<>{out, specs, separator_, grouping_, decimal_point_}); +} #endif -FMT_FUNC std::system_error vsystem_error(int error_code, string_view format_str, +FMT_FUNC std::system_error vsystem_error(int error_code, string_view fmt, format_args args) { auto ec = std::error_code(error_code, std::generic_category()); - return std::system_error(ec, vformat(format_str, args)); + return std::system_error(ec, vformat(fmt, args)); } namespace detail { @@ -143,58 +166,8 @@ FMT_CONSTEXPR inline uint64_t rotr(uint64_t n, uint32_t r) noexcept { return (n >> r) | (n << (64 - r)); } -// Computes 128-bit result of multiplication of two 64-bit unsigned integers. -inline uint128_fallback umul128(uint64_t x, uint64_t y) noexcept { -#if FMT_USE_INT128 - auto p = static_cast(x) * static_cast(y); - return {static_cast(p >> 64), static_cast(p)}; -#elif defined(_MSC_VER) && defined(_M_X64) - auto result = uint128_fallback(); - result.lo_ = _umul128(x, y, &result.hi_); - return result; -#else - const uint64_t mask = static_cast(max_value()); - - uint64_t a = x >> 32; - uint64_t b = x & mask; - uint64_t c = y >> 32; - uint64_t d = y & mask; - - uint64_t ac = a * c; - uint64_t bc = b * c; - uint64_t ad = a * d; - uint64_t bd = b * d; - - uint64_t intermediate = (bd >> 32) + (ad & mask) + (bc & mask); - - return {ac + (intermediate >> 32) + (ad >> 32) + (bc >> 32), - (intermediate << 32) + (bd & mask)}; -#endif -} - // Implementation of Dragonbox algorithm: https://github.com/jk-jeon/dragonbox. namespace dragonbox { -// Computes upper 64 bits of multiplication of two 64-bit unsigned integers. -inline uint64_t umul128_upper64(uint64_t x, uint64_t y) noexcept { -#if FMT_USE_INT128 - auto p = static_cast(x) * static_cast(y); - return static_cast(p >> 64); -#elif defined(_MSC_VER) && defined(_M_X64) - return __umulh(x, y); -#else - return umul128(x, y).high(); -#endif -} - -// Computes upper 128 bits of multiplication of a 64-bit unsigned integer and a -// 128-bit unsigned integer. -inline uint128_fallback umul192_upper128(uint64_t x, - uint128_fallback y) noexcept { - uint128_fallback r = umul128(x, y.high()); - r += umul128_upper64(x, y.low()); - return r; -} - // Computes upper 64 bits of multiplication of a 32-bit unsigned integer and a // 64-bit unsigned integer. inline uint64_t umul96_upper64(uint32_t x, uint64_t y) noexcept { @@ -216,25 +189,13 @@ inline uint64_t umul96_lower64(uint32_t x, uint64_t y) noexcept { return x * y; } -// Computes floor(log10(pow(2, e))) for e in [-2620, 2620] using the method from -// https://fmt.dev/papers/Dragonbox.pdf#page=28, section 6.1. -inline int floor_log10_pow2(int e) noexcept { - FMT_ASSERT(e <= 2620 && e >= -2620, "too large exponent"); - static_assert((-1 >> 1) == -1, "right shift is not arithmetic"); - return (e * 315653) >> 20; -} - // Various fast log computations. -inline int floor_log2_pow10(int e) noexcept { - FMT_ASSERT(e <= 1233 && e >= -1233, "too large exponent"); - return (e * 1741647) >> 19; -} inline int floor_log10_pow2_minus_log10_4_over_3(int e) noexcept { FMT_ASSERT(e <= 2936 && e >= -2985, "too large exponent"); return (e * 631305 - 261663) >> 21; } -static constexpr struct { +FMT_INLINE_VARIABLE constexpr struct { uint32_t divisor; int shift_amount; } div_small_pow10_infos[] = {{10, 16}, {100, 16}}; @@ -288,7 +249,7 @@ inline uint64_t divide_by_10_to_kappa_plus_1(uint64_t n) noexcept { } // Various subroutines using pow10 cache -template struct cache_accessor; +template struct cache_accessor; template <> struct cache_accessor { using carrier_uint = float_info::carrier_uint; @@ -1009,8 +970,23 @@ template <> struct cache_accessor { {0xfcf62c1dee382c42, 0x46729e03dd9ed7b6}, {0x9e19db92b4e31ba9, 0x6c07a2c26a8346d2}, {0xc5a05277621be293, 0xc7098b7305241886}, - { 0xf70867153aa2db38, - 0xb8cbee4fc66d1ea8 } + {0xf70867153aa2db38, 0xb8cbee4fc66d1ea8}, + {0x9a65406d44a5c903, 0x737f74f1dc043329}, + {0xc0fe908895cf3b44, 0x505f522e53053ff3}, + {0xf13e34aabb430a15, 0x647726b9e7c68ff0}, + {0x96c6e0eab509e64d, 0x5eca783430dc19f6}, + {0xbc789925624c5fe0, 0xb67d16413d132073}, + {0xeb96bf6ebadf77d8, 0xe41c5bd18c57e890}, + {0x933e37a534cbaae7, 0x8e91b962f7b6f15a}, + {0xb80dc58e81fe95a1, 0x723627bbb5a4adb1}, + {0xe61136f2227e3b09, 0xcec3b1aaa30dd91d}, + {0x8fcac257558ee4e6, 0x213a4f0aa5e8a7b2}, + {0xb3bd72ed2af29e1f, 0xa988e2cd4f62d19e}, + {0xe0accfa875af45a7, 0x93eb1b80a33b8606}, + {0x8c6c01c9498d8b88, 0xbc72f130660533c4}, + {0xaf87023b9bf0ee6a, 0xeb8fad7c7f8680b5}, + { 0xdb68c2ca82ed2a05, + 0xa67398db9f6820e2 } #else {0xff77b1fcbebcdc4f, 0x25e8e89c13bb0f7b}, {0xce5d73ff402d98e3, 0xfb0a3d212dc81290}, @@ -1034,8 +1010,8 @@ template <> struct cache_accessor { {0x8da471a9de737e24, 0x5ceaecfed289e5d3}, {0xe4d5e82392a40515, 0x0fabaf3feaa5334b}, {0xb8da1662e7b00a17, 0x3d6a751f3b936244}, - { 0x95527a5202df0ccb, - 0x0f37801e0c43ebc9 } + {0x95527a5202df0ccb, 0x0f37801e0c43ebc9}, + {0xf13e34aabb430a15, 0x647726b9e7c68ff0} #endif }; @@ -1138,8 +1114,12 @@ template <> struct cache_accessor { } }; +FMT_FUNC uint128_fallback get_cached_power(int k) noexcept { + return cache_accessor::get_cached_power(k); +} + // Various integer checks -template +template bool is_left_endpoint_integer_shorter_interval(int exponent) noexcept { const int case_shorter_interval_left_endpoint_lower_threshold = 2; const int case_shorter_interval_left_endpoint_upper_threshold = 3; @@ -1148,12 +1128,12 @@ bool is_left_endpoint_integer_shorter_interval(int exponent) noexcept { } // Remove trailing zeros from n and return the number of zeros removed (float) -FMT_INLINE int remove_trailing_zeros(uint32_t& n) noexcept { +FMT_INLINE int remove_trailing_zeros(uint32_t& n, int s = 0) noexcept { FMT_ASSERT(n != 0, ""); - const uint32_t mod_inv_5 = 0xcccccccd; - const uint32_t mod_inv_25 = mod_inv_5 * mod_inv_5; + // Modular inverse of 5 (mod 2^32): (mod_inv_5 * 5) mod 2^32 = 1. + constexpr uint32_t mod_inv_5 = 0xcccccccd; + constexpr uint32_t mod_inv_25 = 0xc28f5c29; // = mod_inv_5 * mod_inv_5 - int s = 0; while (true) { auto q = rotr(n * mod_inv_25, 2); if (q > max_value() / 100) break; @@ -1165,7 +1145,6 @@ FMT_INLINE int remove_trailing_zeros(uint32_t& n) noexcept { n = q; s |= 1; } - return s; } @@ -1179,32 +1158,17 @@ FMT_INLINE int remove_trailing_zeros(uint64_t& n) noexcept { // Is n is divisible by 10^8? if ((nm.high() & ((1ull << (90 - 64)) - 1)) == 0 && nm.low() < magic_number) { - // If yes, work with the quotient. + // If yes, work with the quotient... auto n32 = static_cast(nm.high() >> (90 - 64)); - - const uint32_t mod_inv_5 = 0xcccccccd; - const uint32_t mod_inv_25 = mod_inv_5 * mod_inv_5; - - int s = 8; - while (true) { - auto q = rotr(n32 * mod_inv_25, 2); - if (q > max_value() / 100) break; - n32 = q; - s += 2; - } - auto q = rotr(n32 * mod_inv_5, 1); - if (q <= max_value() / 10) { - n32 = q; - s |= 1; - } - + // ... and use the 32 bit variant of the function + int s = remove_trailing_zeros(n32, 8); n = n32; return s; } // If n is not divisible by 10^8, work with n itself. - const uint64_t mod_inv_5 = 0xcccccccccccccccd; - const uint64_t mod_inv_25 = mod_inv_5 * mod_inv_5; + constexpr uint64_t mod_inv_5 = 0xcccccccccccccccd; + constexpr uint64_t mod_inv_25 = 0x8f5c28f5c28f5c29; // = mod_inv_5 * mod_inv_5 int s = 0; while (true) { @@ -1223,7 +1187,7 @@ FMT_INLINE int remove_trailing_zeros(uint64_t& n) noexcept { } // The main algorithm for shorter interval case -template +template FMT_INLINE decimal_fp shorter_interval_case(int exponent) noexcept { decimal_fp ret_value; // Compute k and beta @@ -1394,17 +1358,6 @@ template decimal_fp to_decimal(T x) noexcept { return ret_value; } } // namespace dragonbox - -#ifdef _MSC_VER -FMT_FUNC auto fmt_snprintf(char* buf, size_t size, const char* fmt, ...) - -> int { - auto args = va_list(); - va_start(args, fmt); - int result = vsnprintf_s(buf, size, _TRUNCATE, fmt, args); - va_end(args); - return result; -} -#endif } // namespace detail template <> struct formatter { @@ -1413,9 +1366,8 @@ template <> struct formatter { return ctx.begin(); } - template - auto format(const detail::bigint& n, FormatContext& ctx) const -> - typename FormatContext::iterator { + auto format(const detail::bigint& n, format_context& ctx) const + -> format_context::iterator { auto out = ctx.out(); bool first = true; for (auto i = n.bigits_.size(); i > 0; --i) { @@ -1474,57 +1426,44 @@ FMT_FUNC std::string vformat(string_view fmt, format_args args) { } namespace detail { -#ifdef _WIN32 +#ifndef _WIN32 +FMT_FUNC bool write_console(std::FILE*, string_view) { return false; } +#else using dword = conditional_t; extern "C" __declspec(dllimport) int __stdcall WriteConsoleW( // void*, const void*, dword, dword*, void*); FMT_FUNC bool write_console(std::FILE* f, string_view text) { auto fd = _fileno(f); - if (_isatty(fd)) { - detail::utf8_to_utf16 u16(string_view(text.data(), text.size())); - auto written = detail::dword(); - if (detail::WriteConsoleW(reinterpret_cast(_get_osfhandle(fd)), - u16.c_str(), static_cast(u16.size()), - &written, nullptr)) { - return true; - } - } - // We return false if the file descriptor was not TTY, or it was but - // SetConsoleW failed which can happen if the output has been redirected to - // NUL. In both cases when we return false, we should attempt to do regular - // write via fwrite or std::ostream::write. - return false; + if (!_isatty(fd)) return false; + auto u16 = utf8_to_utf16(text); + auto written = dword(); + return WriteConsoleW(reinterpret_cast(_get_osfhandle(fd)), u16.c_str(), + static_cast(u16.size()), &written, nullptr) != 0; +} + +// Print assuming legacy (non-Unicode) encoding. +FMT_FUNC void vprint_mojibake(std::FILE* f, string_view fmt, format_args args) { + auto buffer = memory_buffer(); + detail::vformat_to(buffer, fmt, + basic_format_args>(args)); + fwrite_fully(buffer.data(), 1, buffer.size(), f); } #endif FMT_FUNC void print(std::FILE* f, string_view text) { -#ifdef _WIN32 - if (write_console(f, text)) return; -#endif - detail::fwrite_fully(text.data(), 1, text.size(), f); + if (!write_console(f, text)) fwrite_fully(text.data(), 1, text.size(), f); } } // namespace detail -FMT_FUNC void vprint(std::FILE* f, string_view format_str, format_args args) { - memory_buffer buffer; - detail::vformat_to(buffer, format_str, args); +FMT_FUNC void vprint(std::FILE* f, string_view fmt, format_args args) { + auto buffer = memory_buffer(); + detail::vformat_to(buffer, fmt, args); detail::print(f, {buffer.data(), buffer.size()}); } -#ifdef _WIN32 -// Print assuming legacy (non-Unicode) encoding. -FMT_FUNC void detail::vprint_mojibake(std::FILE* f, string_view format_str, - format_args args) { - memory_buffer buffer; - detail::vformat_to(buffer, format_str, - basic_format_args>(args)); - fwrite_fully(buffer.data(), 1, buffer.size(), f); -} -#endif - -FMT_FUNC void vprint(string_view format_str, format_args args) { - vprint(stdout, format_str, args); +FMT_FUNC void vprint(string_view fmt, format_args args) { + vprint(stdout, fmt, args); } namespace detail { diff --git a/contrib/fmt/include/fmt/format.h b/contrib/fmt/include/fmt/format.h index 7c607dbd3..e5bd8b110 100644 --- a/contrib/fmt/include/fmt/format.h +++ b/contrib/fmt/include/fmt/format.h @@ -33,13 +33,14 @@ #ifndef FMT_FORMAT_H_ #define FMT_FORMAT_H_ -#include // std::signbit -#include // uint32_t -#include // std::memcpy -#include // std::numeric_limits -#include // std::uninitialized_copy -#include // std::runtime_error -#include // std::system_error +#include // std::signbit +#include // uint32_t +#include // std::memcpy +#include // std::initializer_list +#include // std::numeric_limits +#include // std::uninitialized_copy +#include // std::runtime_error +#include // std::system_error #ifdef __cpp_lib_bit_cast # include // std::bitcast @@ -47,16 +48,54 @@ #include "core.h" -#if FMT_GCC_VERSION -# define FMT_GCC_VISIBILITY_HIDDEN __attribute__((visibility("hidden"))) +#if defined __cpp_inline_variables && __cpp_inline_variables >= 201606L +# define FMT_INLINE_VARIABLE inline #else -# define FMT_GCC_VISIBILITY_HIDDEN +# define FMT_INLINE_VARIABLE #endif -#ifdef __NVCC__ -# define FMT_CUDA_VERSION (__CUDACC_VER_MAJOR__ * 100 + __CUDACC_VER_MINOR__) +#if FMT_HAS_CPP17_ATTRIBUTE(fallthrough) +# define FMT_FALLTHROUGH [[fallthrough]] +#elif defined(__clang__) +# define FMT_FALLTHROUGH [[clang::fallthrough]] +#elif FMT_GCC_VERSION >= 700 && \ + (!defined(__EDG_VERSION__) || __EDG_VERSION__ >= 520) +# define FMT_FALLTHROUGH [[gnu::fallthrough]] #else -# define FMT_CUDA_VERSION 0 +# define FMT_FALLTHROUGH +#endif + +#ifndef FMT_DEPRECATED +# if FMT_HAS_CPP14_ATTRIBUTE(deprecated) || FMT_MSC_VERSION >= 1900 +# define FMT_DEPRECATED [[deprecated]] +# else +# if (defined(__GNUC__) && !defined(__LCC__)) || defined(__clang__) +# define FMT_DEPRECATED __attribute__((deprecated)) +# elif FMT_MSC_VERSION +# define FMT_DEPRECATED __declspec(deprecated) +# else +# define FMT_DEPRECATED /* deprecated */ +# endif +# endif +#endif + +#ifndef FMT_NO_UNIQUE_ADDRESS +# if FMT_CPLUSPLUS >= 202002L +# if FMT_HAS_CPP_ATTRIBUTE(no_unique_address) +# define FMT_NO_UNIQUE_ADDRESS [[no_unique_address]] +# elif FMT_MSC_VERSION >= 1929 // VS2019 v16.10 and later +# define FMT_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]] +# endif +# endif +#endif +#ifndef FMT_NO_UNIQUE_ADDRESS +# define FMT_NO_UNIQUE_ADDRESS +#endif + +#if FMT_GCC_VERSION || defined(__clang__) +# define FMT_VISIBILITY(value) __attribute__((visibility(value))) +#else +# define FMT_VISIBILITY(value) #endif #ifdef __has_builtin @@ -71,12 +110,6 @@ # define FMT_NOINLINE #endif -#if FMT_MSC_VERSION -# define FMT_MSC_DEFAULT = default -#else -# define FMT_MSC_DEFAULT -#endif - #ifndef FMT_THROW # if FMT_EXCEPTIONS # if FMT_MSC_VERSION || defined(__NVCC__) @@ -95,10 +128,8 @@ FMT_END_NAMESPACE # define FMT_THROW(x) throw x # endif # else -# define FMT_THROW(x) \ - do { \ - FMT_ASSERT(false, (x).what()); \ - } while (false) +# define FMT_THROW(x) \ + ::fmt::detail::assert_fail(__FILE__, __LINE__, (x).what()) # endif #endif @@ -200,7 +231,8 @@ inline auto clzll(uint64_t x) -> int { _BitScanReverse64(&r, x); # else // Scan the high 32 bits. - if (_BitScanReverse(&r, static_cast(x >> 32))) return 63 ^ (r + 32); + if (_BitScanReverse(&r, static_cast(x >> 32))) + return 63 ^ static_cast(r + 32); // Scan the low 32 bits. _BitScanReverse(&r, static_cast(x)); # endif @@ -240,6 +272,19 @@ FMT_END_NAMESPACE #endif FMT_BEGIN_NAMESPACE + +template struct disjunction : std::false_type {}; +template struct disjunction

: P {}; +template +struct disjunction + : conditional_t> {}; + +template struct conjunction : std::true_type {}; +template struct conjunction

: P {}; +template +struct conjunction + : conditional_t, P1> {}; + namespace detail { FMT_CONSTEXPR inline void abort_fuzzing_if(bool condition) { @@ -359,6 +404,10 @@ class uint128_fallback { -> uint128_fallback { return {lhs.hi_ & rhs.hi_, lhs.lo_ & rhs.lo_}; } + friend constexpr auto operator~(const uint128_fallback& n) + -> uint128_fallback { + return {~n.hi_, ~n.lo_}; + } friend auto operator+(const uint128_fallback& lhs, const uint128_fallback& rhs) -> uint128_fallback { auto result = uint128_fallback(lhs); @@ -397,6 +446,10 @@ class uint128_fallback { lo_ = new_lo; hi_ = new_hi; } + FMT_CONSTEXPR void operator&=(uint128_fallback n) { + lo_ &= n.lo_; + hi_ &= n.hi_; + } FMT_CONSTEXPR20 uint128_fallback& operator+=(uint64_t n) noexcept { if (is_constant_evaluated()) { @@ -463,10 +516,34 @@ inline auto bit_cast(const From& from) -> To { return result; } +template +FMT_CONSTEXPR20 inline auto countl_zero_fallback(UInt n) -> int { + int lz = 0; + constexpr UInt msb_mask = static_cast(1) << (num_bits() - 1); + for (; (n & msb_mask) == 0; n <<= 1) lz++; + return lz; +} + +FMT_CONSTEXPR20 inline auto countl_zero(uint32_t n) -> int { +#ifdef FMT_BUILTIN_CLZ + if (!is_constant_evaluated()) return FMT_BUILTIN_CLZ(n); +#endif + return countl_zero_fallback(n); +} + +FMT_CONSTEXPR20 inline auto countl_zero(uint64_t n) -> int { +#ifdef FMT_BUILTIN_CLZLL + if (!is_constant_evaluated()) return FMT_BUILTIN_CLZLL(n); +#endif + return countl_zero_fallback(n); +} + FMT_INLINE void assume(bool condition) { (void)condition; #if FMT_HAS_BUILTIN(__builtin_assume) && !FMT_ICC_VERSION __builtin_assume(condition); +#elif FMT_GCC_VERSION + if (!condition) __builtin_unreachable(); #endif } @@ -485,20 +562,6 @@ inline auto get_data(Container& c) -> typename Container::value_type* { return c.data(); } -#if defined(_SECURE_SCL) && _SECURE_SCL -// Make a checked iterator to avoid MSVC warnings. -template using checked_ptr = stdext::checked_array_iterator; -template -constexpr auto make_checked(T* p, size_t size) -> checked_ptr { - return {p, size}; -} -#else -template using checked_ptr = T*; -template constexpr auto make_checked(T* p, size_t) -> T* { - return p; -} -#endif - // Attempts to reserve space for n extra characters in the output range. // Returns a pointer to the reserved range or a reference to it. template ::value)> @@ -506,12 +569,12 @@ template ::value)> __attribute__((no_sanitize("undefined"))) #endif inline auto -reserve(std::back_insert_iterator it, size_t n) - -> checked_ptr { +reserve(std::back_insert_iterator it, size_t n) -> + typename Container::value_type* { Container& c = get_container(it); size_t size = c.size(); c.resize(size + n); - return make_checked(get_data(c) + size, n); + return get_data(c) + size; } template @@ -543,8 +606,8 @@ template auto to_pointer(buffer_appender it, size_t n) -> T* { } template ::value)> -inline auto base_iterator(std::back_insert_iterator& it, - checked_ptr) +inline auto base_iterator(std::back_insert_iterator it, + typename Container::value_type*) -> std::back_insert_iterator { return it; } @@ -607,7 +670,8 @@ FMT_CONSTEXPR inline auto utf8_decode(const char* s, uint32_t* c, int* e) constexpr const int shiftc[] = {0, 18, 12, 6, 0}; constexpr const int shifte[] = {0, 6, 4, 2, 0}; - int len = code_point_length_impl(*s); + int len = "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0\0\0\2\2\2\2\3\3\4" + [static_cast(*s) >> 3]; // Compute the pointer to the next character early so that the next // iteration can start working on the next character. Neither Clang // nor GCC figure out this reordering on their own. @@ -636,7 +700,7 @@ FMT_CONSTEXPR inline auto utf8_decode(const char* s, uint32_t* c, int* e) return next; } -constexpr uint32_t invalid_code_point = ~uint32_t(); +constexpr FMT_INLINE_VARIABLE uint32_t invalid_code_point = ~uint32_t(); // Invokes f(cp, sv) for every code point cp in s with sv being the string view // corresponding to the code point. cp is invalid_code_point on error. @@ -706,6 +770,7 @@ FMT_CONSTEXPR inline size_t compute_width(string_view s) { return true; } }; + // We could avoid branches by using utf8_decode directly. for_each_codepoint(s, count_code_points{&num_code_points}); return num_code_points; } @@ -737,13 +802,48 @@ inline auto code_point_index(basic_string_view s, size_t n) string_view(reinterpret_cast(s.data()), s.size()), n); } +template struct is_integral : std::is_integral {}; +template <> struct is_integral : std::true_type {}; +template <> struct is_integral : std::true_type {}; + +template +using is_signed = + std::integral_constant::is_signed || + std::is_same::value>; + +template +using is_integer = + bool_constant::value && !std::is_same::value && + !std::is_same::value && + !std::is_same::value>; + +#ifndef FMT_USE_FLOAT +# define FMT_USE_FLOAT 1 +#endif +#ifndef FMT_USE_DOUBLE +# define FMT_USE_DOUBLE 1 +#endif +#ifndef FMT_USE_LONG_DOUBLE +# define FMT_USE_LONG_DOUBLE 1 +#endif + #ifndef FMT_USE_FLOAT128 -# ifdef __SIZEOF_FLOAT128__ -# define FMT_USE_FLOAT128 1 -# else +# ifdef __clang__ +// Clang emulates GCC, so it has to appear early. +# if FMT_HAS_INCLUDE() +# define FMT_USE_FLOAT128 1 +# endif +# elif defined(__GNUC__) +// GNU C++: +# if defined(_GLIBCXX_USE_FLOAT128) && !defined(__STRICT_ANSI__) +# define FMT_USE_FLOAT128 1 +# endif +# endif +# ifndef FMT_USE_FLOAT128 # define FMT_USE_FLOAT128 0 # endif #endif + #if FMT_USE_FLOAT128 using float128 = __float128; #else @@ -775,7 +875,7 @@ void buffer::append(const U* begin, const U* end) { try_reserve(size_ + count); auto free_cap = capacity_ - size_; if (free_cap < count) count = free_cap; - std::uninitialized_copy_n(begin, count, make_checked(ptr_ + size_, count)); + std::uninitialized_copy_n(begin, count, ptr_ + size_); size_ += count; begin += count; } @@ -787,7 +887,7 @@ template struct is_locale> : std::true_type {}; } // namespace detail -FMT_MODULE_EXPORT_BEGIN +FMT_BEGIN_EXPORT // The number of characters to store in the basic_memory_buffer object itself // to avoid dynamic memory allocation. @@ -820,8 +920,8 @@ class basic_memory_buffer final : public detail::buffer { private: T store_[SIZE]; - // Don't inherit from Allocator avoid generating type_info for it. - Allocator alloc_; + // Don't inherit from Allocator to avoid generating type_info for it. + FMT_NO_UNIQUE_ADDRESS Allocator alloc_; // Deallocate memory allocated by the buffer. FMT_CONSTEXPR20 void deallocate() { @@ -830,7 +930,28 @@ class basic_memory_buffer final : public detail::buffer { } protected: - FMT_CONSTEXPR20 void grow(size_t size) override; + FMT_CONSTEXPR20 void grow(size_t size) override { + detail::abort_fuzzing_if(size > 5000); + const size_t max_size = std::allocator_traits::max_size(alloc_); + size_t old_capacity = this->capacity(); + size_t new_capacity = old_capacity + old_capacity / 2; + if (size > new_capacity) + new_capacity = size; + else if (new_capacity > max_size) + new_capacity = size > max_size ? size : max_size; + T* old_data = this->data(); + T* new_data = + std::allocator_traits::allocate(alloc_, new_capacity); + // Suppress a bogus -Wstringop-overflow in gcc 13.1 (#3481). + detail::assume(this->size() <= new_capacity); + // The following code doesn't throw, so the raw pointer above doesn't leak. + std::uninitialized_copy_n(old_data, this->size(), new_data); + this->set(new_data, new_capacity); + // deallocate must not throw according to the standard, but even if it does, + // the buffer already uses the new storage and will deallocate it in + // destructor. + if (old_data != store_) alloc_.deallocate(old_data, old_capacity); + } public: using value_type = T; @@ -852,8 +973,7 @@ class basic_memory_buffer final : public detail::buffer { size_t size = other.size(), capacity = other.capacity(); if (data == other.store_) { this->set(store_, capacity); - detail::copy_str(other.store_, other.store_ + size, - detail::make_checked(store_, capacity)); + detail::copy_str(other.store_, other.store_ + size, store_); } else { this->set(data, capacity); // Set pointer to the inline array so that delete is not called @@ -907,55 +1027,28 @@ class basic_memory_buffer final : public detail::buffer { } }; -template -FMT_CONSTEXPR20 void basic_memory_buffer::grow( - size_t size) { - detail::abort_fuzzing_if(size > 5000); - const size_t max_size = std::allocator_traits::max_size(alloc_); - size_t old_capacity = this->capacity(); - size_t new_capacity = old_capacity + old_capacity / 2; - if (size > new_capacity) - new_capacity = size; - else if (new_capacity > max_size) - new_capacity = size > max_size ? size : max_size; - T* old_data = this->data(); - T* new_data = - std::allocator_traits::allocate(alloc_, new_capacity); - // The following code doesn't throw, so the raw pointer above doesn't leak. - std::uninitialized_copy(old_data, old_data + this->size(), - detail::make_checked(new_data, new_capacity)); - this->set(new_data, new_capacity); - // deallocate must not throw according to the standard, but even if it does, - // the buffer already uses the new storage and will deallocate it in - // destructor. - if (old_data != store_) alloc_.deallocate(old_data, old_capacity); -} - using memory_buffer = basic_memory_buffer; template struct is_contiguous> : std::true_type { }; +FMT_END_EXPORT namespace detail { -#ifdef _WIN32 FMT_API bool write_console(std::FILE* f, string_view text); -#endif FMT_API void print(std::FILE*, string_view); } // namespace detail +FMT_BEGIN_EXPORT + +// Suppress a misleading warning in older versions of clang. +#if FMT_CLANG_VERSION +# pragma clang diagnostic ignored "-Wweak-vtables" +#endif -/** A formatting error such as invalid format string. */ -FMT_CLASS_API -class FMT_API format_error : public std::runtime_error { +/** An error reported from a formatting function. */ +class FMT_VISIBILITY("default") format_error : public std::runtime_error { public: - explicit format_error(const char* message) : std::runtime_error(message) {} - explicit format_error(const std::string& message) - : std::runtime_error(message) {} - format_error(const format_error&) = default; - format_error& operator=(const format_error&) = default; - format_error(format_error&&) = default; - format_error& operator=(format_error&&) = default; - ~format_error() noexcept override FMT_MSC_DEFAULT; + using std::runtime_error::runtime_error; }; namespace detail_exported { @@ -984,16 +1077,52 @@ constexpr auto compile_string_to_view(detail::std_string_view s) } } // namespace detail_exported -FMT_BEGIN_DETAIL_NAMESPACE +class loc_value { + private: + basic_format_arg value_; -template struct is_integral : std::is_integral {}; -template <> struct is_integral : std::true_type {}; -template <> struct is_integral : std::true_type {}; + public: + template ::value)> + loc_value(T value) : value_(detail::make_arg(value)) {} -template -using is_signed = - std::integral_constant::is_signed || - std::is_same::value>; + template ::value)> + loc_value(T) {} + + template auto visit(Visitor&& vis) -> decltype(vis(0)) { + return visit_format_arg(vis, value_); + } +}; + +// A locale facet that formats values in UTF-8. +// It is parameterized on the locale to avoid the heavy include. +template class format_facet : public Locale::facet { + private: + std::string separator_; + std::string grouping_; + std::string decimal_point_; + + protected: + virtual auto do_put(appender out, loc_value val, + const format_specs<>& specs) const -> bool; + + public: + static FMT_API typename Locale::id id; + + explicit format_facet(Locale& loc); + explicit format_facet(string_view sep = "", + std::initializer_list g = {3}, + std::string decimal_point = ".") + : separator_(sep.data(), sep.size()), + grouping_(g.begin(), g.end()), + decimal_point_(decimal_point) {} + + auto put(appender out, loc_value val, const format_specs<>& specs) const + -> bool { + return do_put(out, val, specs); + } +}; + +namespace detail { // Returns true if value is negative, false otherwise. // Same as `value < 0` but doesn't produce warnings if T is an unsigned type. @@ -1122,7 +1251,7 @@ FMT_CONSTEXPR auto count_digits(UInt n) -> int { FMT_INLINE auto do_count_digits(uint32_t n) -> int { // An optimization by Kendall Willets from https://bit.ly/3uOIQrB. // This increments the upper 32 bits (log10(T) - 1) when >= T is added. -# define FMT_INC(T) (((sizeof(# T) - 1ull) << 32) - T) +# define FMT_INC(T) (((sizeof(#T) - 1ull) << 32) - T) static constexpr uint64_t table[] = { FMT_INC(0), FMT_INC(0), FMT_INC(0), // 8 FMT_INC(10), FMT_INC(10), FMT_INC(10), // 64 @@ -1238,7 +1367,7 @@ template format_decimal_result { // Buffer is large enough to hold all digits (digits10 + 1). - Char buffer[digits10() + 1]; + Char buffer[digits10() + 1] = {}; auto end = format_decimal(buffer, value, size).end; return {out, detail::copy_str_noinline(buffer, end, out)}; } @@ -1283,7 +1412,139 @@ class utf8_to_utf16 { auto str() const -> std::wstring { return {&buffer_[0], size()}; } }; +enum class to_utf8_error_policy { abort, replace }; + +// A converter from UTF-16/UTF-32 (host endian) to UTF-8. +template class to_utf8 { + private: + Buffer buffer_; + + public: + to_utf8() {} + explicit to_utf8(basic_string_view s, + to_utf8_error_policy policy = to_utf8_error_policy::abort) { + static_assert(sizeof(WChar) == 2 || sizeof(WChar) == 4, + "Expect utf16 or utf32"); + if (!convert(s, policy)) + FMT_THROW(std::runtime_error(sizeof(WChar) == 2 ? "invalid utf16" + : "invalid utf32")); + } + operator string_view() const { return string_view(&buffer_[0], size()); } + size_t size() const { return buffer_.size() - 1; } + const char* c_str() const { return &buffer_[0]; } + std::string str() const { return std::string(&buffer_[0], size()); } + + // Performs conversion returning a bool instead of throwing exception on + // conversion error. This method may still throw in case of memory allocation + // error. + bool convert(basic_string_view s, + to_utf8_error_policy policy = to_utf8_error_policy::abort) { + if (!convert(buffer_, s, policy)) return false; + buffer_.push_back(0); + return true; + } + static bool convert( + Buffer& buf, basic_string_view s, + to_utf8_error_policy policy = to_utf8_error_policy::abort) { + for (auto p = s.begin(); p != s.end(); ++p) { + uint32_t c = static_cast(*p); + if (sizeof(WChar) == 2 && c >= 0xd800 && c <= 0xdfff) { + // Handle a surrogate pair. + ++p; + if (p == s.end() || (c & 0xfc00) != 0xd800 || (*p & 0xfc00) != 0xdc00) { + if (policy == to_utf8_error_policy::abort) return false; + buf.append(string_view("�")); + --p; + } else { + c = (c << 10) + static_cast(*p) - 0x35fdc00; + } + } else if (c < 0x80) { + buf.push_back(static_cast(c)); + } else if (c < 0x800) { + buf.push_back(static_cast(0xc0 | (c >> 6))); + buf.push_back(static_cast(0x80 | (c & 0x3f))); + } else if ((c >= 0x800 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xffff)) { + buf.push_back(static_cast(0xe0 | (c >> 12))); + buf.push_back(static_cast(0x80 | ((c & 0xfff) >> 6))); + buf.push_back(static_cast(0x80 | (c & 0x3f))); + } else if (c >= 0x10000 && c <= 0x10ffff) { + buf.push_back(static_cast(0xf0 | (c >> 18))); + buf.push_back(static_cast(0x80 | ((c & 0x3ffff) >> 12))); + buf.push_back(static_cast(0x80 | ((c & 0xfff) >> 6))); + buf.push_back(static_cast(0x80 | (c & 0x3f))); + } else { + return false; + } + } + return true; + } +}; + +// Computes 128-bit result of multiplication of two 64-bit unsigned integers. +inline uint128_fallback umul128(uint64_t x, uint64_t y) noexcept { +#if FMT_USE_INT128 + auto p = static_cast(x) * static_cast(y); + return {static_cast(p >> 64), static_cast(p)}; +#elif defined(_MSC_VER) && defined(_M_X64) + auto result = uint128_fallback(); + result.lo_ = _umul128(x, y, &result.hi_); + return result; +#else + const uint64_t mask = static_cast(max_value()); + + uint64_t a = x >> 32; + uint64_t b = x & mask; + uint64_t c = y >> 32; + uint64_t d = y & mask; + + uint64_t ac = a * c; + uint64_t bc = b * c; + uint64_t ad = a * d; + uint64_t bd = b * d; + + uint64_t intermediate = (bd >> 32) + (ad & mask) + (bc & mask); + + return {ac + (intermediate >> 32) + (ad >> 32) + (bc >> 32), + (intermediate << 32) + (bd & mask)}; +#endif +} + namespace dragonbox { +// Computes floor(log10(pow(2, e))) for e in [-2620, 2620] using the method from +// https://fmt.dev/papers/Dragonbox.pdf#page=28, section 6.1. +inline int floor_log10_pow2(int e) noexcept { + FMT_ASSERT(e <= 2620 && e >= -2620, "too large exponent"); + static_assert((-1 >> 1) == -1, "right shift is not arithmetic"); + return (e * 315653) >> 20; +} + +inline int floor_log2_pow10(int e) noexcept { + FMT_ASSERT(e <= 1233 && e >= -1233, "too large exponent"); + return (e * 1741647) >> 19; +} + +// Computes upper 64 bits of multiplication of two 64-bit unsigned integers. +inline uint64_t umul128_upper64(uint64_t x, uint64_t y) noexcept { +#if FMT_USE_INT128 + auto p = static_cast(x) * static_cast(y); + return static_cast(p >> 64); +#elif defined(_MSC_VER) && defined(_M_X64) + return __umulh(x, y); +#else + return umul128(x, y).high(); +#endif +} + +// Computes upper 128 bits of multiplication of a 64-bit unsigned integer and a +// 128-bit unsigned integer. +inline uint128_fallback umul192_upper128(uint64_t x, + uint128_fallback y) noexcept { + uint128_fallback r = umul128(x, y.high()); + r += umul128_upper64(x, y.low()); + return r; +} + +FMT_API uint128_fallback get_cached_power(int k) noexcept; // Type-specific information that Dragonbox uses. template struct float_info; @@ -1307,7 +1568,7 @@ template <> struct float_info { static const int big_divisor = 1000; static const int small_divisor = 100; static const int min_k = -292; - static const int max_k = 326; + static const int max_k = 341; static const int shorter_interval_tie_lower_threshold = -77; static const int shorter_interval_tie_upper_threshold = -77; }; @@ -1354,8 +1615,8 @@ template constexpr int num_significand_bits() { template constexpr auto exponent_mask() -> typename dragonbox::float_info::carrier_uint { - using uint = typename dragonbox::float_info::carrier_uint; - return ((uint(1) << dragonbox::float_info::exponent_bits) - 1) + using float_uint = typename dragonbox::float_info::carrier_uint; + return ((float_uint(1) << dragonbox::float_info::exponent_bits) - 1) << num_significand_bits(); } template constexpr auto exponent_bias() -> int { @@ -1476,157 +1737,31 @@ FMT_CONSTEXPR inline fp operator*(fp x, fp y) { } template struct basic_data { - // Normalized 64-bit significands of pow(10, k), for k = -348, -340, ..., 340. - // These are generated by support/compute-powers.py. - static constexpr uint64_t pow10_significands[87] = { - 0xfa8fd5a0081c0288, 0xbaaee17fa23ebf76, 0x8b16fb203055ac76, - 0xcf42894a5dce35ea, 0x9a6bb0aa55653b2d, 0xe61acf033d1a45df, - 0xab70fe17c79ac6ca, 0xff77b1fcbebcdc4f, 0xbe5691ef416bd60c, - 0x8dd01fad907ffc3c, 0xd3515c2831559a83, 0x9d71ac8fada6c9b5, - 0xea9c227723ee8bcb, 0xaecc49914078536d, 0x823c12795db6ce57, - 0xc21094364dfb5637, 0x9096ea6f3848984f, 0xd77485cb25823ac7, - 0xa086cfcd97bf97f4, 0xef340a98172aace5, 0xb23867fb2a35b28e, - 0x84c8d4dfd2c63f3b, 0xc5dd44271ad3cdba, 0x936b9fcebb25c996, - 0xdbac6c247d62a584, 0xa3ab66580d5fdaf6, 0xf3e2f893dec3f126, - 0xb5b5ada8aaff80b8, 0x87625f056c7c4a8b, 0xc9bcff6034c13053, - 0x964e858c91ba2655, 0xdff9772470297ebd, 0xa6dfbd9fb8e5b88f, - 0xf8a95fcf88747d94, 0xb94470938fa89bcf, 0x8a08f0f8bf0f156b, - 0xcdb02555653131b6, 0x993fe2c6d07b7fac, 0xe45c10c42a2b3b06, - 0xaa242499697392d3, 0xfd87b5f28300ca0e, 0xbce5086492111aeb, - 0x8cbccc096f5088cc, 0xd1b71758e219652c, 0x9c40000000000000, - 0xe8d4a51000000000, 0xad78ebc5ac620000, 0x813f3978f8940984, - 0xc097ce7bc90715b3, 0x8f7e32ce7bea5c70, 0xd5d238a4abe98068, - 0x9f4f2726179a2245, 0xed63a231d4c4fb27, 0xb0de65388cc8ada8, - 0x83c7088e1aab65db, 0xc45d1df942711d9a, 0x924d692ca61be758, - 0xda01ee641a708dea, 0xa26da3999aef774a, 0xf209787bb47d6b85, - 0xb454e4a179dd1877, 0x865b86925b9bc5c2, 0xc83553c5c8965d3d, - 0x952ab45cfa97a0b3, 0xde469fbd99a05fe3, 0xa59bc234db398c25, - 0xf6c69a72a3989f5c, 0xb7dcbf5354e9bece, 0x88fcf317f22241e2, - 0xcc20ce9bd35c78a5, 0x98165af37b2153df, 0xe2a0b5dc971f303a, - 0xa8d9d1535ce3b396, 0xfb9b7cd9a4a7443c, 0xbb764c4ca7a44410, - 0x8bab8eefb6409c1a, 0xd01fef10a657842c, 0x9b10a4e5e9913129, - 0xe7109bfba19c0c9d, 0xac2820d9623bf429, 0x80444b5e7aa7cf85, - 0xbf21e44003acdd2d, 0x8e679c2f5e44ff8f, 0xd433179d9c8cb841, - 0x9e19db92b4e31ba9, 0xeb96bf6ebadf77d9, 0xaf87023b9bf0ee6b, + // For checking rounding thresholds. + // The kth entry is chosen to be the smallest integer such that the + // upper 32-bits of 10^(k+1) times it is strictly bigger than 5 * 10^k. + static constexpr uint32_t fractional_part_rounding_thresholds[8] = { + 2576980378U, // ceil(2^31 + 2^32/10^1) + 2190433321U, // ceil(2^31 + 2^32/10^2) + 2151778616U, // ceil(2^31 + 2^32/10^3) + 2147913145U, // ceil(2^31 + 2^32/10^4) + 2147526598U, // ceil(2^31 + 2^32/10^5) + 2147487943U, // ceil(2^31 + 2^32/10^6) + 2147484078U, // ceil(2^31 + 2^32/10^7) + 2147483691U // ceil(2^31 + 2^32/10^8) }; - -#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409 -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wnarrowing" -#endif - // Binary exponents of pow(10, k), for k = -348, -340, ..., 340, corresponding - // to significands above. - static constexpr int16_t pow10_exponents[87] = { - -1220, -1193, -1166, -1140, -1113, -1087, -1060, -1034, -1007, -980, -954, - -927, -901, -874, -847, -821, -794, -768, -741, -715, -688, -661, - -635, -608, -582, -555, -529, -502, -475, -449, -422, -396, -369, - -343, -316, -289, -263, -236, -210, -183, -157, -130, -103, -77, - -50, -24, 3, 30, 56, 83, 109, 136, 162, 189, 216, - 242, 269, 295, 322, 348, 375, 402, 428, 455, 481, 508, - 534, 561, 588, 614, 641, 667, 694, 720, 747, 774, 800, - 827, 853, 880, 907, 933, 960, 986, 1013, 1039, 1066}; -#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409 -# pragma GCC diagnostic pop -#endif - - static constexpr uint64_t power_of_10_64[20] = { - 1, FMT_POWERS_OF_10(1ULL), FMT_POWERS_OF_10(1000000000ULL), - 10000000000000000000ULL}; }; - -#if FMT_CPLUSPLUS < 201703L -template constexpr uint64_t basic_data::pow10_significands[]; -template constexpr int16_t basic_data::pow10_exponents[]; -template constexpr uint64_t basic_data::power_of_10_64[]; -#endif - // This is a struct rather than an alias to avoid shadowing warnings in gcc. struct data : basic_data<> {}; -// Returns a cached power of 10 `c_k = c_k.f * pow(2, c_k.e)` such that its -// (binary) exponent satisfies `min_exponent <= c_k.e <= min_exponent + 28`. -FMT_CONSTEXPR inline fp get_cached_power(int min_exponent, - int& pow10_exponent) { - const int shift = 32; - // log10(2) = 0x0.4d104d427de7fbcc... - const int64_t significand = 0x4d104d427de7fbcc; - int index = static_cast( - ((min_exponent + fp::num_significand_bits - 1) * (significand >> shift) + - ((int64_t(1) << shift) - 1)) // ceil - >> 32 // arithmetic shift - ); - // Decimal exponent of the first (smallest) cached power of 10. - const int first_dec_exp = -348; - // Difference between 2 consecutive decimal exponents in cached powers of 10. - const int dec_exp_step = 8; - index = (index - first_dec_exp - 1) / dec_exp_step + 1; - pow10_exponent = first_dec_exp + index * dec_exp_step; - // Using *(x + index) instead of x[index] avoids an issue with some compilers - // using the EDG frontend (e.g. nvhpc/22.3 in C++17 mode). - return {*(data::pow10_significands + index), - *(data::pow10_exponents + index)}; -} - -#ifndef _MSC_VER -# define FMT_SNPRINTF snprintf -#else -FMT_API auto fmt_snprintf(char* buf, size_t size, const char* fmt, ...) -> int; -# define FMT_SNPRINTF fmt_snprintf -#endif // _MSC_VER - -// Formats a floating-point number with snprintf using the hexfloat format. +#if FMT_CPLUSPLUS < 201703L template -auto snprintf_float(T value, int precision, float_specs specs, - buffer& buf) -> int { - // Buffer capacity must be non-zero, otherwise MSVC's vsnprintf_s will fail. - FMT_ASSERT(buf.capacity() > buf.size(), "empty buffer"); - FMT_ASSERT(specs.format == float_format::hex, ""); - static_assert(!std::is_same::value, ""); - - // Build the format string. - char format[7]; // The longest format is "%#.*Le". - char* format_ptr = format; - *format_ptr++ = '%'; - if (specs.showpoint) *format_ptr++ = '#'; - if (precision >= 0) { - *format_ptr++ = '.'; - *format_ptr++ = '*'; - } - if (std::is_same()) *format_ptr++ = 'L'; - *format_ptr++ = specs.upper ? 'A' : 'a'; - *format_ptr = '\0'; - - // Format using snprintf. - auto offset = buf.size(); - for (;;) { - auto begin = buf.data() + offset; - auto capacity = buf.capacity() - offset; - abort_fuzzing_if(precision > 100000); - // Suppress the warning about a nonliteral format string. - // Cannot use auto because of a bug in MinGW (#1532). - int (*snprintf_ptr)(char*, size_t, const char*, ...) = FMT_SNPRINTF; - int result = precision >= 0 - ? snprintf_ptr(begin, capacity, format, precision, value) - : snprintf_ptr(begin, capacity, format, value); - if (result < 0) { - // The buffer will grow exponentially. - buf.try_reserve(buf.capacity() + 1); - continue; - } - auto size = to_unsigned(result); - // Size equal to capacity means that the last character was truncated. - if (size < capacity) { - buf.try_resize(size + offset); - return 0; - } - buf.try_reserve(size + offset + 1); // Add 1 for the terminating '\0'. - } -} +constexpr uint32_t basic_data::fractional_part_rounding_thresholds[]; +#endif -template +template () == num_bits()> using convert_float_result = - conditional_t::value || sizeof(T) == sizeof(double), - double, T>; + conditional_t::value || doublish, double, T>; template constexpr auto convert_float(T value) -> convert_float_result { @@ -1649,8 +1784,7 @@ FMT_NOINLINE FMT_CONSTEXPR auto fill(OutputIt it, size_t n, // width: output display width in (terminal) column positions. template -FMT_CONSTEXPR auto write_padded(OutputIt out, - const basic_format_specs& specs, +FMT_CONSTEXPR auto write_padded(OutputIt out, const format_specs& specs, size_t size, size_t width, F&& f) -> OutputIt { static_assert(align == align::left || align == align::right, ""); unsigned spec_width = to_unsigned(specs.width); @@ -1669,15 +1803,14 @@ FMT_CONSTEXPR auto write_padded(OutputIt out, template -constexpr auto write_padded(OutputIt out, const basic_format_specs& specs, +constexpr auto write_padded(OutputIt out, const format_specs& specs, size_t size, F&& f) -> OutputIt { return write_padded(out, specs, size, size, f); } template FMT_CONSTEXPR auto write_bytes(OutputIt out, string_view bytes, - const basic_format_specs& specs) - -> OutputIt { + const format_specs& specs) -> OutputIt { return write_padded( out, specs, bytes.size(), [bytes](reserve_iterator it) { const char* data = bytes.data(); @@ -1686,8 +1819,8 @@ FMT_CONSTEXPR auto write_bytes(OutputIt out, string_view bytes, } template -auto write_ptr(OutputIt out, UIntPtr value, - const basic_format_specs* specs) -> OutputIt { +auto write_ptr(OutputIt out, UIntPtr value, const format_specs* specs) + -> OutputIt { int num_digits = count_digits<4>(value); auto size = to_unsigned(num_digits) + size_t(2); auto write = [=](reserve_iterator it) { @@ -1749,7 +1882,7 @@ inline auto find_escape(const char* begin, const char* end) [] { \ /* Use the hidden visibility as a workaround for a GCC bug (#1973). */ \ /* Use a macro-like name to avoid shadowing warnings. */ \ - struct FMT_GCC_VISIBILITY_HIDDEN FMT_COMPILE_STRING : base { \ + struct FMT_VISIBILITY("hidden") FMT_COMPILE_STRING : base { \ using char_type FMT_MAYBE_UNUSED = fmt::remove_cvref_t; \ FMT_MAYBE_UNUSED FMT_CONSTEXPR explicit \ operator fmt::basic_string_view() const { \ @@ -1806,16 +1939,14 @@ auto write_escaped_cp(OutputIt out, const find_escape_result& escape) *out++ = static_cast('\\'); break; default: - if (is_utf8()) { - if (escape.cp < 0x100) { - return write_codepoint<2, Char>(out, 'x', escape.cp); - } - if (escape.cp < 0x10000) { - return write_codepoint<4, Char>(out, 'u', escape.cp); - } - if (escape.cp < 0x110000) { - return write_codepoint<8, Char>(out, 'U', escape.cp); - } + if (escape.cp < 0x100) { + return write_codepoint<2, Char>(out, 'x', escape.cp); + } + if (escape.cp < 0x10000) { + return write_codepoint<4, Char>(out, 'u', escape.cp); + } + if (escape.cp < 0x110000) { + return write_codepoint<8, Char>(out, 'U', escape.cp); } for (Char escape_char : basic_string_view( escape.begin, to_unsigned(escape.end - escape.begin))) { @@ -1860,8 +1991,7 @@ auto write_escaped_char(OutputIt out, Char v) -> OutputIt { template FMT_CONSTEXPR auto write_char(OutputIt out, Char value, - const basic_format_specs& specs) - -> OutputIt { + const format_specs& specs) -> OutputIt { bool is_debug = specs.type == presentation_type::debug; return write_padded(out, specs, 1, [=](reserve_iterator it) { if (is_debug) return write_escaped_char(it, value); @@ -1871,11 +2001,14 @@ FMT_CONSTEXPR auto write_char(OutputIt out, Char value, } template FMT_CONSTEXPR auto write(OutputIt out, Char value, - const basic_format_specs& specs, - locale_ref loc = {}) -> OutputIt { + const format_specs& specs, locale_ref loc = {}) + -> OutputIt { + // char is formatted as unsigned char for consistency across platforms. + using unsigned_type = + conditional_t::value, unsigned char, unsigned>; return check_char_specs(specs) ? write_char(out, value, specs) - : write(out, static_cast(value), specs, loc); + : write(out, static_cast(value), specs, loc); } // Data for write_int that doesn't depend on output iterator type. It is used to @@ -1885,7 +2018,7 @@ template struct write_int_data { size_t padding; FMT_CONSTEXPR write_int_data(int num_digits, unsigned prefix, - const basic_format_specs& specs) + const format_specs& specs) : size((prefix >> 24) + to_unsigned(num_digits)), padding(0) { if (specs.align == align::numeric) { auto width = to_unsigned(specs.width); @@ -1907,7 +2040,7 @@ template struct write_int_data { template FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, int num_digits, unsigned prefix, - const basic_format_specs& specs, + const format_specs& specs, W write_digits) -> OutputIt { // Slightly faster check for specs.width == 0 && specs.precision == -1. if ((specs.width | (specs.precision + 1)) == 0) { @@ -1930,19 +2063,19 @@ FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, int num_digits, template class digit_grouping { private: - thousands_sep_result sep_; + std::string grouping_; + std::basic_string thousands_sep_; struct next_state { std::string::const_iterator group; int pos; }; - next_state initial_state() const { return {sep_.grouping.begin(), 0}; } + next_state initial_state() const { return {grouping_.begin(), 0}; } // Returns the next digit group separator position. int next(next_state& state) const { - if (!sep_.thousands_sep) return max_value(); - if (state.group == sep_.grouping.end()) - return state.pos += sep_.grouping.back(); + if (thousands_sep_.empty()) return max_value(); + if (state.group == grouping_.end()) return state.pos += grouping_.back(); if (*state.group <= 0 || *state.group == max_value()) return max_value(); state.pos += *state.group++; @@ -1951,14 +2084,15 @@ template class digit_grouping { public: explicit digit_grouping(locale_ref loc, bool localized = true) { - if (localized) - sep_ = thousands_sep(loc); - else - sep_.thousands_sep = Char(); + if (!localized) return; + auto sep = thousands_sep(loc); + grouping_ = sep.grouping; + if (sep.thousands_sep) thousands_sep_.assign(1, sep.thousands_sep); } - explicit digit_grouping(thousands_sep_result sep) : sep_(sep) {} + digit_grouping(std::string grouping, std::basic_string sep) + : grouping_(std::move(grouping)), thousands_sep_(std::move(sep)) {} - Char separator() const { return sep_.thousands_sep; } + bool has_separator() const { return !thousands_sep_.empty(); } int count_separators(int num_digits) const { int count = 0; @@ -1981,7 +2115,9 @@ template class digit_grouping { for (int i = 0, sep_index = static_cast(separators.size() - 1); i < num_digits; ++i) { if (num_digits - i == separators[sep_index]) { - *out++ = separator(); + out = + copy_str(thousands_sep_.data(), + thousands_sep_.data() + thousands_sep_.size(), out); --sep_index; } *out++ = static_cast(digits[to_unsigned(i)]); @@ -1990,10 +2126,11 @@ template class digit_grouping { } }; +// Writes a decimal integer with digit grouping. template -auto write_int_localized(OutputIt out, UInt value, unsigned prefix, - const basic_format_specs& specs, - const digit_grouping& grouping) -> OutputIt { +auto write_int(OutputIt out, UInt value, unsigned prefix, + const format_specs& specs, + const digit_grouping& grouping) -> OutputIt { static_assert(std::is_same, UInt>::value, ""); int num_digits = count_digits(value); char digits[40]; @@ -2010,13 +2147,13 @@ auto write_int_localized(OutputIt out, UInt value, unsigned prefix, }); } -template -auto write_int_localized(OutputIt& out, UInt value, unsigned prefix, - const basic_format_specs& specs, locale_ref loc) - -> bool { - auto grouping = digit_grouping(loc); - out = write_int_localized(out, value, prefix, specs, grouping); - return true; +// Writes a localized value. +FMT_API auto write_loc(appender out, loc_value value, + const format_specs<>& specs, locale_ref loc) -> bool; +template +inline auto write_loc(OutputIt, loc_value, const format_specs&, + locale_ref) -> bool { + return false; } FMT_CONSTEXPR inline void prefix_append(unsigned& prefix, unsigned value) { @@ -2045,21 +2182,37 @@ FMT_CONSTEXPR auto make_write_int_arg(T value, sign_t sign) return {abs_value, prefix}; } +template struct loc_writer { + buffer_appender out; + const format_specs& specs; + std::basic_string sep; + std::string grouping; + std::basic_string decimal_point; + + template ::value)> + auto operator()(T value) -> bool { + auto arg = make_write_int_arg(value, specs.sign); + write_int(out, static_cast>(arg.abs_value), arg.prefix, + specs, digit_grouping(grouping, sep)); + return true; + } + + template ::value)> + auto operator()(T) -> bool { + return false; + } +}; + template FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, write_int_arg arg, - const basic_format_specs& specs, - locale_ref loc) -> OutputIt { + const format_specs& specs, + locale_ref) -> OutputIt { static_assert(std::is_same>::value, ""); auto abs_value = arg.abs_value; auto prefix = arg.prefix; switch (specs.type) { case presentation_type::none: case presentation_type::dec: { - if (specs.localized && - write_int_localized(out, static_cast>(abs_value), - prefix, specs, loc)) { - return out; - } auto num_digits = count_digits(abs_value); return write_int( out, num_digits, prefix, specs, [=](reserve_iterator it) { @@ -2102,13 +2255,13 @@ FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, write_int_arg arg, case presentation_type::chr: return write_char(out, static_cast(abs_value), specs); default: - throw_format_error("invalid type specifier"); + throw_format_error("invalid format specifier"); } return out; } template FMT_CONSTEXPR FMT_NOINLINE auto write_int_noinline( - OutputIt out, write_int_arg arg, const basic_format_specs& specs, + OutputIt out, write_int_arg arg, const format_specs& specs, locale_ref loc) -> OutputIt { return write_int(out, arg, specs, loc); } @@ -2117,8 +2270,9 @@ template ::value && std::is_same>::value)> FMT_CONSTEXPR FMT_INLINE auto write(OutputIt out, T value, - const basic_format_specs& specs, + const format_specs& specs, locale_ref loc) -> OutputIt { + if (specs.localized && write_loc(out, value, specs, loc)) return out; return write_int_noinline(out, make_write_int_arg(value, specs.sign), specs, loc); } @@ -2128,8 +2282,9 @@ template ::value && !std::is_same>::value)> FMT_CONSTEXPR FMT_INLINE auto write(OutputIt out, T value, - const basic_format_specs& specs, + const format_specs& specs, locale_ref loc) -> OutputIt { + if (specs.localized && write_loc(out, value, specs, loc)) return out; return write_int(out, make_write_int_arg(value, specs.sign), specs, loc); } @@ -2175,7 +2330,7 @@ class counting_iterator { template FMT_CONSTEXPR auto write(OutputIt out, basic_string_view s, - const basic_format_specs& specs) -> OutputIt { + const format_specs& specs) -> OutputIt { auto data = s.data(); auto size = s.size(); if (specs.precision >= 0 && to_unsigned(specs.precision) < size) @@ -2197,16 +2352,15 @@ FMT_CONSTEXPR auto write(OutputIt out, basic_string_view s, template FMT_CONSTEXPR auto write(OutputIt out, basic_string_view> s, - const basic_format_specs& specs, locale_ref) + const format_specs& specs, locale_ref) -> OutputIt { - check_string_type_spec(specs.type); return write(out, s, specs); } template FMT_CONSTEXPR auto write(OutputIt out, const Char* s, - const basic_format_specs& specs, locale_ref) + const format_specs& specs, locale_ref) -> OutputIt { - return check_cstring_type_spec(specs.type) + return specs.type != presentation_type::pointer ? write(out, basic_string_view(s), specs, {}) : write_ptr(out, bit_cast(s), &specs); } @@ -2233,9 +2387,114 @@ FMT_CONSTEXPR auto write(OutputIt out, T value) -> OutputIt { return base_iterator(out, it); } +// DEPRECATED! +template +FMT_CONSTEXPR auto parse_align(const Char* begin, const Char* end, + format_specs& specs) -> const Char* { + FMT_ASSERT(begin != end, ""); + auto align = align::none; + auto p = begin + code_point_length(begin); + if (end - p <= 0) p = begin; + for (;;) { + switch (to_ascii(*p)) { + case '<': + align = align::left; + break; + case '>': + align = align::right; + break; + case '^': + align = align::center; + break; + } + if (align != align::none) { + if (p != begin) { + auto c = *begin; + if (c == '}') return begin; + if (c == '{') { + throw_format_error("invalid fill character '{'"); + return begin; + } + specs.fill = {begin, to_unsigned(p - begin)}; + begin = p + 1; + } else { + ++begin; + } + break; + } else if (p == begin) { + break; + } + p = begin; + } + specs.align = align; + return begin; +} + +// A floating-point presentation format. +enum class float_format : unsigned char { + general, // General: exponent notation or fixed point based on magnitude. + exp, // Exponent notation with the default precision of 6, e.g. 1.2e-3. + fixed, // Fixed point with the default precision of 6, e.g. 0.0012. + hex +}; + +struct float_specs { + int precision; + float_format format : 8; + sign_t sign : 8; + bool upper : 1; + bool locale : 1; + bool binary32 : 1; + bool showpoint : 1; +}; + +template +FMT_CONSTEXPR auto parse_float_type_spec(const format_specs& specs, + ErrorHandler&& eh = {}) + -> float_specs { + auto result = float_specs(); + result.showpoint = specs.alt; + result.locale = specs.localized; + switch (specs.type) { + case presentation_type::none: + result.format = float_format::general; + break; + case presentation_type::general_upper: + result.upper = true; + FMT_FALLTHROUGH; + case presentation_type::general_lower: + result.format = float_format::general; + break; + case presentation_type::exp_upper: + result.upper = true; + FMT_FALLTHROUGH; + case presentation_type::exp_lower: + result.format = float_format::exp; + result.showpoint |= specs.precision != 0; + break; + case presentation_type::fixed_upper: + result.upper = true; + FMT_FALLTHROUGH; + case presentation_type::fixed_lower: + result.format = float_format::fixed; + result.showpoint |= specs.precision != 0; + break; + case presentation_type::hexfloat_upper: + result.upper = true; + FMT_FALLTHROUGH; + case presentation_type::hexfloat_lower: + result.format = float_format::hex; + break; + default: + eh.on_error("invalid format specifier"); + break; + } + return result; +} + template FMT_CONSTEXPR20 auto write_nonfinite(OutputIt out, bool isnan, - basic_format_specs specs, + format_specs specs, const float_specs& fspecs) -> OutputIt { auto str = isnan ? (fspecs.upper ? "NAN" : "nan") : (fspecs.upper ? "INF" : "inf"); @@ -2281,7 +2540,7 @@ template FMT_CONSTEXPR20 auto write_significand(OutputIt out, T significand, int significand_size, int exponent, const Grouping& grouping) -> OutputIt { - if (!grouping.separator()) { + if (!grouping.has_separator()) { out = write_significand(out, significand, significand_size); return detail::fill_n(out, exponent, static_cast('0')); } @@ -2343,7 +2602,7 @@ FMT_CONSTEXPR20 auto write_significand(OutputIt out, T significand, int significand_size, int integral_size, Char decimal_point, const Grouping& grouping) -> OutputIt { - if (!grouping.separator()) { + if (!grouping.has_separator()) { return write_significand(out, significand, significand_size, integral_size, decimal_point); } @@ -2359,7 +2618,7 @@ FMT_CONSTEXPR20 auto write_significand(OutputIt out, T significand, template > FMT_CONSTEXPR20 auto do_write_float(OutputIt out, const DecimalFP& f, - const basic_format_specs& specs, + const format_specs& specs, float_specs fspecs, locale_ref loc) -> OutputIt { auto significand = f.significand; @@ -2418,7 +2677,7 @@ FMT_CONSTEXPR20 auto do_write_float(OutputIt out, const DecimalFP& f, abort_fuzzing_if(num_zeros > 5000); if (fspecs.showpoint) { ++size; - if (num_zeros <= 0 && fspecs.format != float_format::fixed) num_zeros = 1; + if (num_zeros <= 0 && fspecs.format != float_format::fixed) num_zeros = 0; if (num_zeros > 0) size += to_unsigned(num_zeros); } auto grouping = Grouping(loc, fspecs.locale); @@ -2436,7 +2695,7 @@ FMT_CONSTEXPR20 auto do_write_float(OutputIt out, const DecimalFP& f, int num_zeros = fspecs.showpoint ? fspecs.precision - significand_size : 0; size += 1 + to_unsigned(num_zeros > 0 ? num_zeros : 0); auto grouping = Grouping(loc, fspecs.locale); - size += to_unsigned(grouping.count_separators(significand_size)); + size += to_unsigned(grouping.count_separators(exp)); return write_padded(out, specs, size, [&](iterator it) { if (sign) *it++ = detail::sign(sign); it = write_significand(it, significand, significand_size, exp, @@ -2466,7 +2725,7 @@ template class fallback_digit_grouping { public: constexpr fallback_digit_grouping(locale_ref, bool) {} - constexpr Char separator() const { return Char(); } + constexpr bool has_separator() const { return false; } constexpr int count_separators(int) const { return 0; } @@ -2478,7 +2737,7 @@ template class fallback_digit_grouping { template FMT_CONSTEXPR20 auto write_float(OutputIt out, const DecimalFP& f, - const basic_format_specs& specs, + const format_specs& specs, float_specs fspecs, locale_ref loc) -> OutputIt { if (is_constant_evaluated()) { @@ -2506,14 +2765,14 @@ template ::value&& FMT_CONSTEXPR20 bool isfinite(T value) { constexpr T inf = T(std::numeric_limits::infinity()); if (is_constant_evaluated()) - return !detail::isnan(value) && value != inf && value != -inf; + return !detail::isnan(value) && value < inf && value > -inf; return std::isfinite(value); } template ::value)> FMT_CONSTEXPR bool isfinite(T value) { T inf = T(std::numeric_limits::infinity()); // std::isfinite doesn't support __float128. - return !detail::isnan(value) && value != inf && value != -inf; + return !detail::isnan(value) && value < inf && value > -inf; } template ::value)> @@ -2529,78 +2788,6 @@ FMT_INLINE FMT_CONSTEXPR bool signbit(T value) { return std::signbit(static_cast(value)); } -enum class round_direction { unknown, up, down }; - -// Given the divisor (normally a power of 10), the remainder = v % divisor for -// some number v and the error, returns whether v should be rounded up, down, or -// whether the rounding direction can't be determined due to error. -// error should be less than divisor / 2. -FMT_CONSTEXPR inline round_direction get_round_direction(uint64_t divisor, - uint64_t remainder, - uint64_t error) { - FMT_ASSERT(remainder < divisor, ""); // divisor - remainder won't overflow. - FMT_ASSERT(error < divisor, ""); // divisor - error won't overflow. - FMT_ASSERT(error < divisor - error, ""); // error * 2 won't overflow. - // Round down if (remainder + error) * 2 <= divisor. - if (remainder <= divisor - remainder && error * 2 <= divisor - remainder * 2) - return round_direction::down; - // Round up if (remainder - error) * 2 >= divisor. - if (remainder >= error && - remainder - error >= divisor - (remainder - error)) { - return round_direction::up; - } - return round_direction::unknown; -} - -namespace digits { -enum result { - more, // Generate more digits. - done, // Done generating digits. - error // Digit generation cancelled due to an error. -}; -} - -struct gen_digits_handler { - char* buf; - int size; - int precision; - int exp10; - bool fixed; - - FMT_CONSTEXPR digits::result on_digit(char digit, uint64_t divisor, - uint64_t remainder, uint64_t error, - bool integral) { - FMT_ASSERT(remainder < divisor, ""); - buf[size++] = digit; - if (!integral && error >= remainder) return digits::error; - if (size < precision) return digits::more; - if (!integral) { - // Check if error * 2 < divisor with overflow prevention. - // The check is not needed for the integral part because error = 1 - // and divisor > (1 << 32) there. - if (error >= divisor || error >= divisor - error) return digits::error; - } else { - FMT_ASSERT(error == 1 && divisor > 2, ""); - } - auto dir = get_round_direction(divisor, remainder, error); - if (dir != round_direction::up) - return dir == round_direction::down ? digits::done : digits::error; - ++buf[size - 1]; - for (int i = size - 1; i > 0 && buf[i] > '9'; --i) { - buf[i] = '0'; - ++buf[i - 1]; - } - if (buf[0] > '9') { - buf[0] = '1'; - if (fixed) - buf[size++] = '0'; - else - ++exp10; - } - return digits::done; - } -}; - inline FMT_CONSTEXPR20 void adjust_precision(int& precision, int exp10) { // Adjust fixed precision by exponent because it is relative to decimal // point. @@ -2609,101 +2796,6 @@ inline FMT_CONSTEXPR20 void adjust_precision(int& precision, int exp10) { precision += exp10; } -// Generates output using the Grisu digit-gen algorithm. -// error: the size of the region (lower, upper) outside of which numbers -// definitely do not round to value (Delta in Grisu3). -FMT_INLINE FMT_CONSTEXPR20 auto grisu_gen_digits(fp value, uint64_t error, - int& exp, - gen_digits_handler& handler) - -> digits::result { - const fp one(1ULL << -value.e, value.e); - // The integral part of scaled value (p1 in Grisu) = value / one. It cannot be - // zero because it contains a product of two 64-bit numbers with MSB set (due - // to normalization) - 1, shifted right by at most 60 bits. - auto integral = static_cast(value.f >> -one.e); - FMT_ASSERT(integral != 0, ""); - FMT_ASSERT(integral == value.f >> -one.e, ""); - // The fractional part of scaled value (p2 in Grisu) c = value % one. - uint64_t fractional = value.f & (one.f - 1); - exp = count_digits(integral); // kappa in Grisu. - // Non-fixed formats require at least one digit and no precision adjustment. - if (handler.fixed) { - adjust_precision(handler.precision, exp + handler.exp10); - // Check if precision is satisfied just by leading zeros, e.g. - // format("{:.2f}", 0.001) gives "0.00" without generating any digits. - if (handler.precision <= 0) { - if (handler.precision < 0) return digits::done; - // Divide by 10 to prevent overflow. - uint64_t divisor = data::power_of_10_64[exp - 1] << -one.e; - auto dir = get_round_direction(divisor, value.f / 10, error * 10); - if (dir == round_direction::unknown) return digits::error; - handler.buf[handler.size++] = dir == round_direction::up ? '1' : '0'; - return digits::done; - } - } - // Generate digits for the integral part. This can produce up to 10 digits. - do { - uint32_t digit = 0; - auto divmod_integral = [&](uint32_t divisor) { - digit = integral / divisor; - integral %= divisor; - }; - // This optimization by Milo Yip reduces the number of integer divisions by - // one per iteration. - switch (exp) { - case 10: - divmod_integral(1000000000); - break; - case 9: - divmod_integral(100000000); - break; - case 8: - divmod_integral(10000000); - break; - case 7: - divmod_integral(1000000); - break; - case 6: - divmod_integral(100000); - break; - case 5: - divmod_integral(10000); - break; - case 4: - divmod_integral(1000); - break; - case 3: - divmod_integral(100); - break; - case 2: - divmod_integral(10); - break; - case 1: - digit = integral; - integral = 0; - break; - default: - FMT_ASSERT(false, "invalid number of digits"); - } - --exp; - auto remainder = (static_cast(integral) << -one.e) + fractional; - auto result = handler.on_digit(static_cast('0' + digit), - data::power_of_10_64[exp] << -one.e, - remainder, error, true); - if (result != digits::more) return result; - } while (exp > 0); - // Generate digits for the fractional part. - for (;;) { - fractional *= 10; - error *= 10; - char digit = static_cast('0' + (fractional >> -one.e)); - fractional &= one.f - 1; - --exp; - auto result = handler.on_digit(digit, one.f, fractional, error, false); - if (result != digits::more) return result; - } -} - class bigint { private: // A bigint is stored as an array of bigits (big digits), with bigit at index @@ -2804,7 +2896,7 @@ class bigint { auto size = other.bigits_.size(); bigits_.resize(size); auto data = other.bigits_.data(); - std::copy(data, data + size, make_checked(bigits_.data(), size)); + copy_str(data, data + size, bigits_.data()); exp_ = other.exp_; } @@ -3018,6 +3110,7 @@ FMT_CONSTEXPR20 inline void format_dragon(basic_fp value, } int even = static_cast((value.f & 1) == 0); if (!upper) upper = &lower; + bool shortest = num_digits < 0; if ((flags & dragon::fixup) != 0) { if (add_compare(numerator, *upper, denominator) + even <= 0) { --exp10; @@ -3030,7 +3123,7 @@ FMT_CONSTEXPR20 inline void format_dragon(basic_fp value, if ((flags & dragon::fixed) != 0) adjust_precision(num_digits, exp10 + 1); } // Invariant: value == (numerator / denominator) * pow(10, exp10). - if (num_digits < 0) { + if (shortest) { // Generate the shortest representation. num_digits = 0; char* data = buf.data(); @@ -3060,7 +3153,7 @@ FMT_CONSTEXPR20 inline void format_dragon(basic_fp value, } // Generate the given number of digits. exp10 -= num_digits - 1; - if (num_digits == 0) { + if (num_digits <= 0) { denominator *= 10; auto digit = add_compare(numerator, numerator, denominator) > 0 ? '1' : '0'; buf.push_back(digit); @@ -3085,7 +3178,8 @@ FMT_CONSTEXPR20 inline void format_dragon(basic_fp value, } if (buf[0] == overflow) { buf[0] = '1'; - ++exp10; + if ((flags & dragon::fixed) != 0) buf.push_back('0'); + else ++exp10; } return; } @@ -3094,6 +3188,94 @@ FMT_CONSTEXPR20 inline void format_dragon(basic_fp value, buf[num_digits - 1] = static_cast('0' + digit); } +// Formats a floating-point number using the hexfloat format. +template ::value)> +FMT_CONSTEXPR20 void format_hexfloat(Float value, int precision, + float_specs specs, buffer& buf) { + // float is passed as double to reduce the number of instantiations and to + // simplify implementation. + static_assert(!std::is_same::value, ""); + + using info = dragonbox::float_info; + + // Assume Float is in the format [sign][exponent][significand]. + using carrier_uint = typename info::carrier_uint; + + constexpr auto num_float_significand_bits = + detail::num_significand_bits(); + + basic_fp f(value); + f.e += num_float_significand_bits; + if (!has_implicit_bit()) --f.e; + + constexpr auto num_fraction_bits = + num_float_significand_bits + (has_implicit_bit() ? 1 : 0); + constexpr auto num_xdigits = (num_fraction_bits + 3) / 4; + + constexpr auto leading_shift = ((num_xdigits - 1) * 4); + const auto leading_mask = carrier_uint(0xF) << leading_shift; + const auto leading_xdigit = + static_cast((f.f & leading_mask) >> leading_shift); + if (leading_xdigit > 1) f.e -= (32 - countl_zero(leading_xdigit) - 1); + + int print_xdigits = num_xdigits - 1; + if (precision >= 0 && print_xdigits > precision) { + const int shift = ((print_xdigits - precision - 1) * 4); + const auto mask = carrier_uint(0xF) << shift; + const auto v = static_cast((f.f & mask) >> shift); + + if (v >= 8) { + const auto inc = carrier_uint(1) << (shift + 4); + f.f += inc; + f.f &= ~(inc - 1); + } + + // Check long double overflow + if (!has_implicit_bit()) { + const auto implicit_bit = carrier_uint(1) << num_float_significand_bits; + if ((f.f & implicit_bit) == implicit_bit) { + f.f >>= 4; + f.e += 4; + } + } + + print_xdigits = precision; + } + + char xdigits[num_bits() / 4]; + detail::fill_n(xdigits, sizeof(xdigits), '0'); + format_uint<4>(xdigits, f.f, num_xdigits, specs.upper); + + // Remove zero tail + while (print_xdigits > 0 && xdigits[print_xdigits] == '0') --print_xdigits; + + buf.push_back('0'); + buf.push_back(specs.upper ? 'X' : 'x'); + buf.push_back(xdigits[0]); + if (specs.showpoint || print_xdigits > 0 || print_xdigits < precision) + buf.push_back('.'); + buf.append(xdigits + 1, xdigits + 1 + print_xdigits); + for (; print_xdigits < precision; ++print_xdigits) buf.push_back('0'); + + buf.push_back(specs.upper ? 'P' : 'p'); + + uint32_t abs_e; + if (f.e < 0) { + buf.push_back('-'); + abs_e = static_cast(-f.e); + } else { + buf.push_back('+'); + abs_e = static_cast(f.e); + } + format_decimal(appender(buf), abs_e, detail::count_digits(abs_e)); +} + +template ::value)> +FMT_CONSTEXPR20 void format_hexfloat(Float value, int precision, + float_specs specs, buffer& buf) { + format_hexfloat(static_cast(value), precision, specs, buf); +} + template FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs, buffer& buf) -> int { @@ -3116,7 +3298,7 @@ FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs, int exp = 0; bool use_dragon = true; unsigned dragon_flags = 0; - if (!is_fast_float()) { + if (!is_fast_float() || is_constant_evaluated()) { const auto inv_log2_10 = 0.3010299956639812; // 1 / log2(10) using info = dragonbox::float_info; const auto f = basic_fp(converted_value); @@ -3124,10 +3306,11 @@ FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs, // 10^(exp - 1) <= value < 10^exp or 10^exp <= value < 10^(exp + 1). // This is based on log10(value) == log2(value) / log2(10) and approximation // of log2(value) by e + num_fraction_bits idea from double-conversion. - exp = static_cast( - std::ceil((f.e + count_digits<1>(f.f) - 1) * inv_log2_10 - 1e-10)); + auto e = (f.e + count_digits<1>(f.f) - 1) * inv_log2_10 - 1e-10; + exp = static_cast(e); + if (e > exp) ++exp; // Compute ceil. dragon_flags = dragon::fixup; - } else if (!is_constant_evaluated() && precision < 0) { + } else if (precision < 0) { // Use Dragonbox for the shortest format. if (specs.binary32) { auto dec = dragonbox::to_decimal(static_cast(value)); @@ -3138,23 +3321,244 @@ FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs, write(buffer_appender(buf), dec.significand); return dec.exponent; } else { - // Use Grisu + Dragon4 for the given precision: - // https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf. - const int min_exp = -60; // alpha in Grisu. - int cached_exp10 = 0; // K in Grisu. - fp normalized = normalize(fp(converted_value)); - const auto cached_pow = get_cached_power( - min_exp - (normalized.e + fp::num_significand_bits), cached_exp10); - normalized = normalized * cached_pow; - gen_digits_handler handler{buf.data(), 0, precision, -cached_exp10, fixed}; - if (grisu_gen_digits(normalized, 1, exp, handler) != digits::error && - !is_constant_evaluated()) { - exp += handler.exp10; - buf.try_resize(to_unsigned(handler.size)); - use_dragon = false; + // Extract significand bits and exponent bits. + using info = dragonbox::float_info; + auto br = bit_cast(static_cast(value)); + + const uint64_t significand_mask = + (static_cast(1) << num_significand_bits()) - 1; + uint64_t significand = (br & significand_mask); + int exponent = static_cast((br & exponent_mask()) >> + num_significand_bits()); + + if (exponent != 0) { // Check if normal. + exponent -= exponent_bias() + num_significand_bits(); + significand |= + (static_cast(1) << num_significand_bits()); + significand <<= 1; } else { - exp += handler.size - cached_exp10 - 1; - precision = handler.precision; + // Normalize subnormal inputs. + FMT_ASSERT(significand != 0, "zeros should not appear here"); + int shift = countl_zero(significand); + FMT_ASSERT(shift >= num_bits() - num_significand_bits(), + ""); + shift -= (num_bits() - num_significand_bits() - 2); + exponent = (std::numeric_limits::min_exponent - + num_significand_bits()) - + shift; + significand <<= shift; + } + + // Compute the first several nonzero decimal significand digits. + // We call the number we get the first segment. + const int k = info::kappa - dragonbox::floor_log10_pow2(exponent); + exp = -k; + const int beta = exponent + dragonbox::floor_log2_pow10(k); + uint64_t first_segment; + bool has_more_segments; + int digits_in_the_first_segment; + { + const auto r = dragonbox::umul192_upper128( + significand << beta, dragonbox::get_cached_power(k)); + first_segment = r.high(); + has_more_segments = r.low() != 0; + + // The first segment can have 18 ~ 19 digits. + if (first_segment >= 1000000000000000000ULL) { + digits_in_the_first_segment = 19; + } else { + // When it is of 18-digits, we align it to 19-digits by adding a bogus + // zero at the end. + digits_in_the_first_segment = 18; + first_segment *= 10; + } + } + + // Compute the actual number of decimal digits to print. + if (fixed) adjust_precision(precision, exp + digits_in_the_first_segment); + + // Use Dragon4 only when there might be not enough digits in the first + // segment. + if (digits_in_the_first_segment > precision) { + use_dragon = false; + + if (precision <= 0) { + exp += digits_in_the_first_segment; + + if (precision < 0) { + // Nothing to do, since all we have are just leading zeros. + buf.try_resize(0); + } else { + // We may need to round-up. + buf.try_resize(1); + if ((first_segment | static_cast(has_more_segments)) > + 5000000000000000000ULL) { + buf[0] = '1'; + } else { + buf[0] = '0'; + } + } + } // precision <= 0 + else { + exp += digits_in_the_first_segment - precision; + + // When precision > 0, we divide the first segment into three + // subsegments, each with 9, 9, and 0 ~ 1 digits so that each fits + // in 32-bits which usually allows faster calculation than in + // 64-bits. Since some compiler (e.g. MSVC) doesn't know how to optimize + // division-by-constant for large 64-bit divisors, we do it here + // manually. The magic number 7922816251426433760 below is equal to + // ceil(2^(64+32) / 10^10). + const uint32_t first_subsegment = static_cast( + dragonbox::umul128_upper64(first_segment, 7922816251426433760ULL) >> + 32); + const uint64_t second_third_subsegments = + first_segment - first_subsegment * 10000000000ULL; + + uint64_t prod; + uint32_t digits; + bool should_round_up; + int number_of_digits_to_print = precision > 9 ? 9 : precision; + + // Print a 9-digits subsegment, either the first or the second. + auto print_subsegment = [&](uint32_t subsegment, char* buffer) { + int number_of_digits_printed = 0; + + // If we want to print an odd number of digits from the subsegment, + if ((number_of_digits_to_print & 1) != 0) { + // Convert to 64-bit fixed-point fractional form with 1-digit + // integer part. The magic number 720575941 is a good enough + // approximation of 2^(32 + 24) / 10^8; see + // https://jk-jeon.github.io/posts/2022/12/fixed-precision-formatting/#fixed-length-case + // for details. + prod = ((subsegment * static_cast(720575941)) >> 24) + 1; + digits = static_cast(prod >> 32); + *buffer = static_cast('0' + digits); + number_of_digits_printed++; + } + // If we want to print an even number of digits from the + // first_subsegment, + else { + // Convert to 64-bit fixed-point fractional form with 2-digits + // integer part. The magic number 450359963 is a good enough + // approximation of 2^(32 + 20) / 10^7; see + // https://jk-jeon.github.io/posts/2022/12/fixed-precision-formatting/#fixed-length-case + // for details. + prod = ((subsegment * static_cast(450359963)) >> 20) + 1; + digits = static_cast(prod >> 32); + copy2(buffer, digits2(digits)); + number_of_digits_printed += 2; + } + + // Print all digit pairs. + while (number_of_digits_printed < number_of_digits_to_print) { + prod = static_cast(prod) * static_cast(100); + digits = static_cast(prod >> 32); + copy2(buffer + number_of_digits_printed, digits2(digits)); + number_of_digits_printed += 2; + } + }; + + // Print first subsegment. + print_subsegment(first_subsegment, buf.data()); + + // Perform rounding if the first subsegment is the last subsegment to + // print. + if (precision <= 9) { + // Rounding inside the subsegment. + // We round-up if: + // - either the fractional part is strictly larger than 1/2, or + // - the fractional part is exactly 1/2 and the last digit is odd. + // We rely on the following observations: + // - If fractional_part >= threshold, then the fractional part is + // strictly larger than 1/2. + // - If the MSB of fractional_part is set, then the fractional part + // must be at least 1/2. + // - When the MSB of fractional_part is set, either + // second_third_subsegments being nonzero or has_more_segments + // being true means there are further digits not printed, so the + // fractional part is strictly larger than 1/2. + if (precision < 9) { + uint32_t fractional_part = static_cast(prod); + should_round_up = fractional_part >= + data::fractional_part_rounding_thresholds + [8 - number_of_digits_to_print] || + ((fractional_part >> 31) & + ((digits & 1) | (second_third_subsegments != 0) | + has_more_segments)) != 0; + } + // Rounding at the subsegment boundary. + // In this case, the fractional part is at least 1/2 if and only if + // second_third_subsegments >= 5000000000ULL, and is strictly larger + // than 1/2 if we further have either second_third_subsegments > + // 5000000000ULL or has_more_segments == true. + else { + should_round_up = second_third_subsegments > 5000000000ULL || + (second_third_subsegments == 5000000000ULL && + ((digits & 1) != 0 || has_more_segments)); + } + } + // Otherwise, print the second subsegment. + else { + // Compilers are not aware of how to leverage the maximum value of + // second_third_subsegments to find out a better magic number which + // allows us to eliminate an additional shift. 1844674407370955162 = + // ceil(2^64/10) < ceil(2^64*(10^9/(10^10 - 1))). + const uint32_t second_subsegment = + static_cast(dragonbox::umul128_upper64( + second_third_subsegments, 1844674407370955162ULL)); + const uint32_t third_subsegment = + static_cast(second_third_subsegments) - + second_subsegment * 10; + + number_of_digits_to_print = precision - 9; + print_subsegment(second_subsegment, buf.data() + 9); + + // Rounding inside the subsegment. + if (precision < 18) { + // The condition third_subsegment != 0 implies that the segment was + // of 19 digits, so in this case the third segment should be + // consisting of a genuine digit from the input. + uint32_t fractional_part = static_cast(prod); + should_round_up = fractional_part >= + data::fractional_part_rounding_thresholds + [8 - number_of_digits_to_print] || + ((fractional_part >> 31) & + ((digits & 1) | (third_subsegment != 0) | + has_more_segments)) != 0; + } + // Rounding at the subsegment boundary. + else { + // In this case, the segment must be of 19 digits, thus + // the third subsegment should be consisting of a genuine digit from + // the input. + should_round_up = third_subsegment > 5 || + (third_subsegment == 5 && + ((digits & 1) != 0 || has_more_segments)); + } + } + + // Round-up if necessary. + if (should_round_up) { + ++buf[precision - 1]; + for (int i = precision - 1; i > 0 && buf[i] > '9'; --i) { + buf[i] = '0'; + ++buf[i - 1]; + } + if (buf[0] > '9') { + buf[0] = '1'; + if (fixed) + buf[precision++] = '0'; + else + ++exp; + } + } + buf.try_resize(to_unsigned(precision)); + } + } // if (digits_in_the_first_segment > precision) + else { + // Adjust the exponent for its use in Dragon4. + exp += digits_in_the_first_segment - 1; } } if (use_dragon) { @@ -3181,13 +3585,10 @@ FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs, } return exp; } - -template ::value)> -FMT_CONSTEXPR20 auto write(OutputIt out, T value, - basic_format_specs specs, locale_ref loc = {}) +template +FMT_CONSTEXPR20 auto write_float(OutputIt out, T value, + format_specs specs, locale_ref loc) -> OutputIt { - if (const_check(!is_supported_floating_point(value))) return out; float_specs fspecs = parse_float_type_spec(specs); fspecs.sign = specs.sign; if (detail::signbit(value)) { // value < 0 is false for NaN so use signbit. @@ -3211,7 +3612,7 @@ FMT_CONSTEXPR20 auto write(OutputIt out, T value, memory_buffer buffer; if (fspecs.format == float_format::hex) { if (fspecs.sign) buffer.push_back(detail::sign(fspecs.sign)); - snprintf_float(convert_float(value), specs.precision, fspecs, buffer); + format_hexfloat(convert_float(value), specs.precision, fspecs, buffer); return write_bytes(out, {buffer.data(), buffer.size()}, specs); } @@ -3233,11 +3634,20 @@ FMT_CONSTEXPR20 auto write(OutputIt out, T value, return write_float(out, f, specs, fspecs, loc); } +template ::value)> +FMT_CONSTEXPR20 auto write(OutputIt out, T value, format_specs specs, + locale_ref loc = {}) -> OutputIt { + if (const_check(!is_supported_floating_point(value))) return out; + return specs.localized && write_loc(out, value, specs, loc) + ? out + : write_float(out, value, specs, loc); +} + template ::value)> FMT_CONSTEXPR20 auto write(OutputIt out, T value) -> OutputIt { - if (is_constant_evaluated()) - return write(out, value, basic_format_specs()); + if (is_constant_evaluated()) return write(out, value, format_specs()); if (const_check(!is_supported_floating_point(value))) return out; auto fspecs = float_specs(); @@ -3246,11 +3656,11 @@ FMT_CONSTEXPR20 auto write(OutputIt out, T value) -> OutputIt { value = -value; } - constexpr auto specs = basic_format_specs(); + constexpr auto specs = format_specs(); using floaty = conditional_t::value, double, T>; - using uint = typename dragonbox::float_info::carrier_uint; - uint mask = exponent_mask(); - if ((bit_cast(value) & mask) == mask) + using floaty_uint = typename dragonbox::float_info::carrier_uint; + floaty_uint mask = exponent_mask(); + if ((bit_cast(value) & mask) == mask) return write_nonfinite(out, std::isnan(value), specs, fspecs); auto dec = dragonbox::to_decimal(static_cast(value)); @@ -3261,12 +3671,12 @@ template ::value && !is_fast_float::value)> inline auto write(OutputIt out, T value) -> OutputIt { - return write(out, value, basic_format_specs()); + return write(out, value, format_specs()); } template -auto write(OutputIt out, monostate, basic_format_specs = {}, - locale_ref = {}) -> OutputIt { +auto write(OutputIt out, monostate, format_specs = {}, locale_ref = {}) + -> OutputIt { FMT_ASSERT(false, ""); return out; } @@ -3300,8 +3710,8 @@ FMT_CONSTEXPR auto write(OutputIt out, T value) -> OutputIt { template ::value)> FMT_CONSTEXPR auto write(OutputIt out, T value, - const basic_format_specs& specs = {}, - locale_ref = {}) -> OutputIt { + const format_specs& specs = {}, locale_ref = {}) + -> OutputIt { return specs.type != presentation_type::none && specs.type != presentation_type::string ? write(out, value ? 1 : 0, specs, {}) @@ -3318,20 +3728,15 @@ FMT_CONSTEXPR auto write(OutputIt out, Char value) -> OutputIt { template FMT_CONSTEXPR_CHAR_TRAITS auto write(OutputIt out, const Char* value) -> OutputIt { - if (!value) { - throw_format_error("string pointer is null"); - } else { - out = write(out, basic_string_view(value)); - } + if (value) return write(out, basic_string_view(value)); + throw_format_error("string pointer is null"); return out; } template ::value)> -auto write(OutputIt out, const T* value, - const basic_format_specs& specs = {}, locale_ref = {}) - -> OutputIt { - check_pointer_type_spec(specs.type, error_handler()); +auto write(OutputIt out, const T* value, const format_specs& specs = {}, + locale_ref = {}) -> OutputIt { return write_ptr(out, bit_cast(value), &specs); } @@ -3341,8 +3746,8 @@ template enable_if_t< std::is_class::value && !is_string::value && !is_floating_point::value && !std::is_same::value && - !std::is_same().map(value))>::value, + !std::is_same().map( + value))>>::value, OutputIt> { return write(out, arg_mapper().map(value)); } @@ -3352,12 +3757,8 @@ template enable_if_t::value == type::custom_type, OutputIt> { - using formatter_type = - conditional_t::value, - typename Context::template formatter_type, - fallback_formatter>; auto ctx = Context(out, {}, {}); - return formatter_type().format(value, ctx); + return typename Context::template formatter_type().format(value, ctx); } // An argument visitor that formats the argument and writes it via the output @@ -3386,7 +3787,7 @@ template struct arg_formatter { using context = buffer_context; iterator out; - const basic_format_specs& specs; + const format_specs& specs; locale_ref locale; template @@ -3411,12 +3812,6 @@ template struct custom_formatter { template void operator()(T) const {} }; -template -using is_integer = - bool_constant::value && !std::is_same::value && - !std::is_same::value && - !std::is_same::value>; - template class width_checker { public: explicit FMT_CONSTEXPR width_checker(ErrorHandler& eh) : handler_(eh) {} @@ -3466,55 +3861,12 @@ FMT_CONSTEXPR auto get_dynamic_spec(FormatArg arg, ErrorHandler eh) -> int { } template -FMT_CONSTEXPR auto get_arg(Context& ctx, ID id) -> - typename Context::format_arg { +FMT_CONSTEXPR auto get_arg(Context& ctx, ID id) -> decltype(ctx.arg(id)) { auto arg = ctx.arg(id); if (!arg) ctx.on_error("argument not found"); return arg; } -// The standard format specifier handler with checking. -template class specs_handler : public specs_setter { - private: - basic_format_parse_context& parse_context_; - buffer_context& context_; - - // This is only needed for compatibility with gcc 4.4. - using format_arg = basic_format_arg>; - - FMT_CONSTEXPR auto get_arg(auto_id) -> format_arg { - return detail::get_arg(context_, parse_context_.next_arg_id()); - } - - FMT_CONSTEXPR auto get_arg(int arg_id) -> format_arg { - parse_context_.check_arg_id(arg_id); - return detail::get_arg(context_, arg_id); - } - - FMT_CONSTEXPR auto get_arg(basic_string_view arg_id) -> format_arg { - parse_context_.check_arg_id(arg_id); - return detail::get_arg(context_, arg_id); - } - - public: - FMT_CONSTEXPR specs_handler(basic_format_specs& specs, - basic_format_parse_context& parse_ctx, - buffer_context& ctx) - : specs_setter(specs), parse_context_(parse_ctx), context_(ctx) {} - - template FMT_CONSTEXPR void on_dynamic_width(Id arg_id) { - this->specs_.width = get_dynamic_spec( - get_arg(arg_id), context_.error_handler()); - } - - template FMT_CONSTEXPR void on_dynamic_precision(Id arg_id) { - this->specs_.precision = get_dynamic_spec( - get_arg(arg_id), context_.error_handler()); - } - - void on_error(const char* message) { context_.on_error(message); } -}; - template