From fb1f193254f273c20fcb0d9c4147a4849068808a Mon Sep 17 00:00:00 2001 From: Rochet2 Date: Wed, 29 Jul 2026 04:34:45 +0300 Subject: [PATCH 1/3] Post-2.0.1 cleanup: docs, find_package, parser hygiene. Correct README/ASSUMPTIONS accuracy, ship CMake Config for find_package, accept tab whitespace in tables, and drop unused @-cycle memo leftovers. --- .gitignore | 4 +++ ASSUMPTIONS.md | 2 +- CHANGELOG.md | 13 +++++++ CMakeLists.txt | 17 +++++++++ README.md | 27 ++++++++------ cmake/smallfolk_cppConfig.cmake.in | 5 +++ smallfolk.cpp | 57 ++++++++++-------------------- smallfolk.h | 3 +- tests/test_smallfolk.cpp | 7 ++++ 9 files changed, 83 insertions(+), 52 deletions(-) create mode 100644 cmake/smallfolk_cppConfig.cmake.in diff --git a/.gitignore b/.gitignore index dce1571..fb4a005 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,10 @@ # Build directories build/ +build-*/ bin/ cmake-build-*/ out/ + +# Local scratch +tmp_*.cpp diff --git a/ASSUMPTIONS.md b/ASSUMPTIONS.md index 48c6c8a..fbea58c 100644 --- a/ASSUMPTIONS.md +++ b/ASSUMPTIONS.md @@ -49,7 +49,7 @@ This document records behavioral assumptions baked into smallfolk_cpp. If you re ## Locale and platform - Number parsing uses the `"C"` locale via `std::strtod` to avoid locale-dependent decimal separators. -- `sprintf` / `snprintf` formatting for number output uses `% .17g` (Lua-minimum style precision for finite values). +- Number output uses `std::snprintf` with `%.17g` (Lua-minimum style precision for finite values). ## Security diff --git a/CHANGELOG.md b/CHANGELOG.md index 04af4ce..b13297f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project are documented in this file. +## [Unreleased] + +### Fixed + +- README accuracy (table-cycle note, install headers, API examples); ASSUMPTIONS number-format note. +- Table parse accepts tab whitespace around `:` / `,` (same as top-level skip). +- CMake install provides `find_package(smallfolk_cpp)` Config/Version files. +- Remove unused `@`-cycle memo/`TABLES` leftovers from dump/load paths. + +### Changed + +- `.gitignore` covers `build-*/` and local `tmp_*.cpp` scratch files. + ## [2.0.1] - 2026-05-31 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 5faee78..3a956c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,4 +87,21 @@ install(EXPORT smallfolk_cppTargets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/smallfolk_cpp ) +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/smallfolk_cppConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/smallfolk_cppConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/smallfolk_cppConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/smallfolk_cpp +) +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/smallfolk_cppConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/smallfolk_cppConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/smallfolk_cpp +) + smallfolk_add_static_analysis_targets() diff --git a/README.md b/README.md index 5a1d754..9243431 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,12 @@ ctest --test-dir build --output-on-failure # if you enable CTest cmake --install build --prefix /path/to/prefix ``` -This installs `smallfolk.h`, the `smallfolk` library, and a CMake export file under `lib/cmake/smallfolk_cpp/`. +This installs `smallfolk.h`, `smallfolk_convert.h`, `smallfolk_schema.h`, the `smallfolk` library, and CMake package files under `lib/cmake/smallfolk_cpp/` so consumers can use `find_package`: + +```cmake +find_package(smallfolk_cpp REQUIRED) +target_link_libraries(my_app PRIVATE smallfolk_cpp::smallfolk) +``` ## Usage @@ -55,7 +60,7 @@ This installs `smallfolk.h`, the `smallfolk` library, and a CMake export file un // create a lua table and set some values to it LuaVal table = LuaVal::table(); -table[1] = "Hello"; // the values is automatically converted to LuaVal +table[1] = "Hello"; // the value is automatically converted to LuaVal table["test"] = "world"; table[67.5] = -234.5; @@ -102,7 +107,7 @@ The benchmark serializes this sample payload: ## Table cycles -**Note: This feature was disabled cause of difficult implementing in C++and possibly unwanted infinite cycles. All table assigning create copies now in the C++ code and no @ notation is recognised for serializing or deserializing. Any such references are set to nil when deserializing. Any @ references are otherwise deep copies in the C++ code** +**Note: This feature was disabled because of difficult implementing in C++ and possibly unwanted infinite cycles. All table assigning creates deep copies now in the C++ code and no `@` notation is recognized for serializing or deserializing. Input containing `@` references is rejected on load. Assigning a table into itself (or as a key) always deep-copies; there are no shared cycles.** From original smallfolk @@ -303,7 +308,7 @@ LuaVal t5 = {1,2, "test", vec}; // Resulting table: {1,2,"test",{{"a","b"},{"a","b"}}} ``` -Creating sequences is easy, but creating complex tables that contain different types of values can be difficult or take a lot of space in code. To avoid quirks and for conveience you can deserialize strings to create values in a compact way. Here two equivalent values are created with normal style and deserialization: +Creating sequences is easy, but creating complex tables that contain different types of values can be difficult or take a lot of space in code. To avoid quirks and for convenience you can deserialize strings to create values in a compact way. Here two equivalent values are created with normal style and deserialization: ```c++ LuaVal val1 = { 1,2, LuaVal::mrg({3,4.5}, LuaVal::LuaTable({{"ke","test"}})) }; @@ -323,9 +328,9 @@ May throw if LuaVal is not valid for some reason (which should not be possible). ### typetag -There are definitions for typetags used to identify each value type. These can be used in the constructor of a LuaValue as well. -For example a table can be created with `LuaValue table(TTABLE)`. You can get the typetag of an object with the member function `LuaTypeTag LuaVal::typetag()`. -GetTypeTag does not throw. +There are definitions for typetags used to identify each value type. These can be used in the constructor of a `LuaVal` as well. +For example a table can be created with `LuaVal table(TTABLE)`. You can get the typetag of an object with the member function `LuaTypeTag LuaVal::typetag()`. +`typetag()` does not throw. ```C++ enum LuaTypeTag @@ -429,7 +434,7 @@ Variadic segments (`try_get_path("a", "b", 1)`) and `std::initializer_list // va_start #include // std::hash #include -#include namespace { @@ -139,8 +138,6 @@ namespace namespace Serializer { - typedef std::vector TABLES; - typedef std::unordered_map MEMO; typedef std::stringstream ACC; inline bool is_nan_value(double value) @@ -190,15 +187,15 @@ namespace Serializer return arr; } - unsigned int dump_type_table(LuaVal const & object, unsigned int nmemo, MEMO& memo, ACC& acc); - unsigned int dump_object(LuaVal const & object, unsigned int nmemo, MEMO& memo, ACC& acc); + void dump_type_table(LuaVal const & object, ACC& acc); + void dump_object(LuaVal const & object, ACC& acc); std::string escape_quotes(const std::string &before, char quote); std::string unescape_quotes(const std::string &before, char quote); bool nonzero_digit(char c); bool is_digit(char c); char strat(std::string const & string, std::string::size_type i); LuaVal expect_number(std::string const & string, size_t& start, ParseContext & ctx); - LuaVal expect_object(std::string const & string, size_t& i, TABLES& tables, ParseContext & ctx); + LuaVal expect_object(std::string const & string, size_t& i, ParseContext & ctx); } LoadLimits const & LuaVal::default_load_limits() @@ -871,9 +868,7 @@ std::string LuaVal::dumps(std::string * errmsg) const { Serializer::ACC acc; acc << std::setprecision(17); // min lua precision - unsigned int nmemo = 0; - Serializer::MEMO memo; - Serializer::dump_object(*this, nmemo, memo, acc); + Serializer::dump_object(*this, acc); return acc.str(); } catch (smallfolk_exception const & e) @@ -907,10 +902,9 @@ LuaVal LuaVal::loads(std::string const & string, LoadLimits const & limits, std: "load limit exceeded: max input size %zu", limits.max_input_size); - Serializer::TABLES tables; ParseContext ctx{ limits, 0, 0 }; size_t i = 0; - LuaVal result = Serializer::expect_object(string, i, tables, ctx); + LuaVal result = Serializer::expect_object(string, i, ctx); skip_whitespace(string, i); if (limits.require_consumed_input && i != string.length()) throw smallfolk_exception("unexpected trailing input at position %zu", i); @@ -976,22 +970,11 @@ LuaVal& LuaVal::operator=(LuaVal const& val) return *this; } -unsigned int Serializer::dump_type_table(LuaVal const & object, unsigned int nmemo, MEMO & memo, ACC & acc) +void Serializer::dump_type_table(LuaVal const & object, ACC & acc) { if (!object.istable()) throw smallfolk_exception("using dump_type_table on non table object"); - /* - // @ circular table references are disabled; deep copy on assign avoids shared refs. - auto it = memo.find(object); - if (it != memo.end()) - { - acc << '@' << it->second; - return nmemo; - } - memo[object] = ++nmemo; - */ - acc << '{'; bool first = true; std::map arr; @@ -1011,27 +994,26 @@ unsigned int Serializer::dump_type_table(LuaVal const & object, unsigned int nme first = false; if (v.first != i) { - nmemo = dump_object(v.first, nmemo, memo, acc); + dump_object(v.first, acc); acc << ':'; } else ++i; - nmemo = dump_object(*v.second, nmemo, memo, acc); + dump_object(*v.second, acc); } for (auto&& v : hash) { if (!first) acc << ','; first = false; - nmemo = dump_object(*v.first, nmemo, memo, acc); + dump_object(*v.first, acc); acc << ':'; - nmemo = dump_object(*v.second, nmemo, memo, acc); + dump_object(*v.second, acc); } acc << '}'; - return nmemo; } -unsigned int Serializer::dump_object(LuaVal const & object, unsigned int nmemo, MEMO & memo, ACC & acc) +void Serializer::dump_object(LuaVal const & object, ACC & acc) { switch (object.typetag()) { @@ -1050,11 +1032,11 @@ unsigned int Serializer::dump_object(LuaVal const & object, unsigned int nmemo, append_number_token(acc, object.num()); break; case TTABLE: - return dump_type_table(object, nmemo, memo, acc); + dump_type_table(object, acc); + break; default: throw smallfolk_exception("dump_object invalid or unhandled tag %i", object.typetag()); } - return nmemo; } std::string Serializer::escape_quotes(const std::string & before, char quote) @@ -1194,7 +1176,7 @@ LuaVal Serializer::expect_number(std::string const & string, size_t & start, Par return value; } -LuaVal Serializer::expect_object(std::string const & string, size_t & i, Serializer::TABLES & tables, ParseContext & ctx) +LuaVal Serializer::expect_object(std::string const & string, size_t & i, ParseContext & ctx) { char cc = strat(string, i++); switch (cc) @@ -1202,7 +1184,7 @@ LuaVal Serializer::expect_object(std::string const & string, size_t & i, Seriali case ' ': case '\t': // skip whitespace - return expect_object(string, i, tables, ctx); + return expect_object(string, i, ctx); case 't': ctx.on_value_created(); return true; @@ -1285,7 +1267,6 @@ LuaVal Serializer::expect_object(std::string const & string, size_t & i, Seriali LuaVal nt(TTABLE); ctx.on_value_created(); unsigned int j = 1; - tables.push_back(nt); if (strat(string, i) == '}') { ++i; @@ -1301,13 +1282,13 @@ LuaVal Serializer::expect_object(std::string const & string, size_t & i, Seriali ctx.limits.max_table_entries); } - LuaVal k = expect_object(string, i, tables, ctx); + LuaVal k = expect_object(string, i, ctx); char at = strat(string, i); - while (at == ' ') + while (at == ' ' || at == '\t') at = strat(string, ++i); if (at == ':') { - nt.set(k, expect_object(string, ++i, tables, ctx)); + nt.set(k, expect_object(string, ++i, ctx)); } else { @@ -1315,7 +1296,7 @@ LuaVal Serializer::expect_object(std::string const & string, size_t & i, Seriali ++j; } char head = strat(string, i); - while (head == ' ') + while (head == ' ' || head == '\t') head = strat(string, ++i); if (head == ',') ++i; diff --git a/smallfolk.h b/smallfolk.h index ba642e5..3a8f375 100644 --- a/smallfolk.h +++ b/smallfolk.h @@ -86,8 +86,7 @@ class LuaVal }; typedef std::unordered_map LuaTable; - // Circular reference memleak if insert self to self (deep copy on assign avoids sharing). - typedef std::unique_ptr TblPtr; + typedef std::unique_ptr TblPtr; // Table assign deep-copies; @ circular refs unsupported. LuaVal(const LuaTypeTag tag) : tag(tag), tbl_ptr(tag == TTABLE ? new LuaTable() : nullptr), d(0), b(false) {} LuaVal() : tag(TTABLE), tbl_ptr(new LuaTable()), d(0), b(false) {} diff --git a/tests/test_smallfolk.cpp b/tests/test_smallfolk.cpp index c186b31..da3f627 100644 --- a/tests/test_smallfolk.cpp +++ b/tests/test_smallfolk.cpp @@ -713,6 +713,13 @@ static void test_lua_smallfolk_interop_wires() expect_true(value.get(2).num() == 2.0, "lua wire tab whitespace value"); } + { + LuaVal value = LuaVal::loads("{\"a\"\t:\t1\t,\t\"b\":2}", &err); + expect_true(err.empty(), "lua wire tab around colon/comma loads"); + expect_true(value.get(LuaVal("a")).num() == 1.0, "lua wire tab colon key a"); + expect_true(value.get(LuaVal("b")).num() == 2.0, "lua wire tab colon key b"); + } + { LuaVal original = LuaVal::loads("{1,2,{3,4.5,'ke':'test'}}", &err); expect_true(err.empty(), "interop round-trip source loads"); From 41ce312640f5c930335b9e4408eadbf68bd696fe Mon Sep 17 00:00:00 2001 From: Rochet2 Date: Wed, 29 Jul 2026 04:48:37 +0300 Subject: [PATCH 2/3] Drop CHANGELOG [Unreleased] section before merge. Keep released history only until the next versioned release. --- CHANGELOG.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b13297f..04af4ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,6 @@ All notable changes to this project are documented in this file. -## [Unreleased] - -### Fixed - -- README accuracy (table-cycle note, install headers, API examples); ASSUMPTIONS number-format note. -- Table parse accepts tab whitespace around `:` / `,` (same as top-level skip). -- CMake install provides `find_package(smallfolk_cpp)` Config/Version files. -- Remove unused `@`-cycle memo/`TABLES` leftovers from dump/load paths. - -### Changed - -- `.gitignore` covers `build-*/` and local `tmp_*.cpp` scratch files. - ## [2.0.1] - 2026-05-31 ### Added From ff9ea65978f2f6d2f922f2011b0077d9e472fa4f Mon Sep 17 00:00:00 2001 From: Rochet2 Date: Wed, 29 Jul 2026 04:52:15 +0300 Subject: [PATCH 3/3] Keep table :/, separators space-only for parse speed. Revert tab skipping in those hot loops; document the whitespace rule in ASSUMPTIONS. --- ASSUMPTIONS.md | 2 +- smallfolk.cpp | 4 ++-- tests/test_smallfolk.cpp | 7 ------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/ASSUMPTIONS.md b/ASSUMPTIONS.md index fbea58c..858c4cf 100644 --- a/ASSUMPTIONS.md +++ b/ASSUMPTIONS.md @@ -9,7 +9,7 @@ This document records behavioral assumptions baked into smallfolk_cpp. If you re - Numbers are stored internally as `double`. Integer values outside exact `double` range may lose precision on round-trip. - Non-finite floats use Smallfolk's single-letter encodings (`I`, `i`, `N`, `Q`) rather than JSON-style `Infinity`/`NaN`. Set `LoadLimits::reject_non_finite_numbers` to reject these during `loads()`. - String keys and values use `"` or `'` quoting; embedded quotes are doubled. There is no `\` escape syntax. -- Whitespace between tokens is limited to space and tab. Other whitespace (newlines, `\r`) is not skipped unless explicitly present in a string literal. +- Whitespace between tokens is limited to space and tab at value boundaries. Between a table key and `:` / `,` / `}`, only spaces are skipped (not tabs), to keep the hot path tight. - Table keys that are positive integers with no fractional part serialize as array elements when consecutive from `1`. Gaps or non-integer numeric keys use explicit `key:value` form. - **`loads()` assumes the input is trusted only to the extent configured by `LoadLimits`.** Default limits cap input size, nesting depth, value count, per-table entry count, and per-string length. Trailing garbage after a valid value is rejected by default. diff --git a/smallfolk.cpp b/smallfolk.cpp index e389087..8f93977 100644 --- a/smallfolk.cpp +++ b/smallfolk.cpp @@ -1284,7 +1284,7 @@ LuaVal Serializer::expect_object(std::string const & string, size_t & i, ParseCo LuaVal k = expect_object(string, i, ctx); char at = strat(string, i); - while (at == ' ' || at == '\t') + while (at == ' ') at = strat(string, ++i); if (at == ':') { @@ -1296,7 +1296,7 @@ LuaVal Serializer::expect_object(std::string const & string, size_t & i, ParseCo ++j; } char head = strat(string, i); - while (head == ' ' || head == '\t') + while (head == ' ') head = strat(string, ++i); if (head == ',') ++i; diff --git a/tests/test_smallfolk.cpp b/tests/test_smallfolk.cpp index da3f627..c186b31 100644 --- a/tests/test_smallfolk.cpp +++ b/tests/test_smallfolk.cpp @@ -713,13 +713,6 @@ static void test_lua_smallfolk_interop_wires() expect_true(value.get(2).num() == 2.0, "lua wire tab whitespace value"); } - { - LuaVal value = LuaVal::loads("{\"a\"\t:\t1\t,\t\"b\":2}", &err); - expect_true(err.empty(), "lua wire tab around colon/comma loads"); - expect_true(value.get(LuaVal("a")).num() == 1.0, "lua wire tab colon key a"); - expect_true(value.get(LuaVal("b")).num() == 2.0, "lua wire tab colon key b"); - } - { LuaVal original = LuaVal::loads("{1,2,{3,4.5,'ke':'test'}}", &err); expect_true(err.empty(), "interop round-trip source loads");