Skip to content

feat(codec): add yaml-cpp codec wrapper - #123

Closed
16bit-ykiko wants to merge 3 commits into
mainfrom
feat/codec-yaml
Closed

feat(codec): add yaml-cpp codec wrapper#123
16bit-ykiko wants to merge 3 commits into
mainfrom
feat/codec-yaml

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Apr 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Wraps yaml-cpp (0.9.0) as a proper codec module following the TOML pattern
  • Serializer builds YAML::Node trees via the streaming codec interface
  • Deserializer reads from YAML::Node, using node_slot wrapper to avoid yaml-cpp's destructive operator= semantics on shared nodes
  • Convenience API: parse() / to_string() / from_yaml() / to_yaml()
  • CMake integration with KOTA_CODEC_ENABLE_YAML option (auto-enabled by KOTA_BUILD_ALL_TESTS)
  • 12 roundtrip tests covering structs, nested vectors, scalars, optionals, error cases, and dynamic DOM capture

Test plan

  • All 888 tests pass (876 existing + 12 new yaml codec tests)
  • Struct with nested vector roundtrip (exercises node_slot fix)
  • Parse from YAML string and re-serialize roundtrip
  • Error cases: parse error, type mismatch

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added YAML serialization and deserialization support as an optional codec module
    • Introduced snapshot testing framework with support for JSON and YAML content comparison, automatically tracking snapshot revisions during test execution

16bit-ykiko and others added 2 commits April 24, 2026 14:14
Wraps yaml-cpp (0.9.0) as a proper codec module following the TOML pattern:
- Serializer builds YAML::Node trees via streaming interface
- Deserializer reads from YAML::Node with node_slot to avoid yaml-cpp's
  destructive operator= semantics on shared nodes
- Convenience API: parse/to_string/from_yaml/to_yaml
- CMake integration with KOTA_CODEC_ENABLE_YAML option
- 12 roundtrip tests covering structs, vectors, scalars, optionals, errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pport

Adds snapshot testing inspired by Rust's insta crate:
- EXPECT_SNAPSHOT/ASSERT_SNAPSHOT for any pretty_dump-able value
- EXPECT_JSON_SNAPSHOT/ASSERT_JSON_SNAPSHOT via codec::json serialization
- EXPECT_YAML_SNAPSHOT/ASSERT_YAML_SNAPSHOT via codec::content -> YAML
- EXPECT_INLINE_SNAPSHOT/ASSERT_INLINE_SNAPSHOT for inline comparisons
- SNAPSHOT_GLOB for iterating files matching a pattern
- Snapshot files stored in __snapshots__/ with ZEST_UPDATE_SNAPSHOTS=1 to update
- TestContext tracks suite/test/file/counter per test for path computation
- 18 tests covering all snapshot modes and utilities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR introduces a new YAML codec for kota with Serializer/Deserializer implementations conforming to streaming serialization protocols, alongside a comprehensive snapshot testing system for kota::zest that tracks per-test context, manages snapshot files, and provides comparison macros with JSON/YAML formatting support.

Changes

Cohort / File(s) Summary
YAML Codec Build Configuration
CMakeLists.txt, src/codec/CMakeLists.txt
Added KOTA_CODEC_ENABLE_YAML CMake option and conditional yaml-cpp dependency integration with external git source and interface library target setup.
YAML Error Handling
include/kota/codec/yaml/error.h
Defined error_kind scoped enum with success/failure categories, error_message constexpr mapper, and type alias binding to generic serde_error.
YAML Serializer
include/kota/codec/yaml/serializer.h
Implemented Serializer<Config> class with stack-based frame tracking for nested maps/sequences, scalar type writers, object/array framing APIs, and DOM node construction via dom()/to_yaml().
YAML Deserializer
include/kota/codec/yaml/deserializer.h
Implemented Deserializer<Config> class with streaming begin_object/next_field/end_object and begin_array/next_element/end_array APIs, node classification for variant dispatch, scalar readers with range narrowing, and from_yaml() overloads.
YAML Convenience API
include/kota/codec/yaml/yaml.h
Added parse_node(), parse<T>(), and to_string<T>() wrapper functions with exception handling control, plus deserialize_traits specialization for YAML::Node deserialization.
Snapshot Testing Registry
include/kota/zest/detail/registry.h, include/kota/zest/detail/suite.h
Added TestContext structure with per-thread tracking of suite/test name, source file, and snapshot counter; populated during test execution.
Snapshot Testing API
include/kota/zest/snap.h
Defined snapshot testing infrastructure with check()/check_inline() functions, snapshot path generation, JSON prettification, YAML serialization helpers, glob pattern matching, and assertion macros (EXPECT_SNAPSHOT, ASSERT_SNAPSHOT, EXPECT_JSON_SNAPSHOT, EXPECT_YAML_SNAPSHOT, etc.).
Snapshot Testing Implementation
src/zest/snap.cpp
Implemented snapshot runtime with file I/O management, ZEST_UPDATE_SNAPSHOTS-controlled update logic, line-based colored diff rendering, JSON prettification with indentation, and YAML value formatting with quoting/escaping rules.
Test Build Configuration
src/zest/CMakeLists.txt, tests/CMakeLists.txt
Added snap.cpp source compilation and conditional YAML codec test inclusion based on KOTA_CODEC_ENABLE_YAML flag.
Test Coverage
tests/unit/codec/yaml/yaml_tests.cpp, tests/unit/zest/snap_test.cpp
Added unit tests validating YAML roundtrip serialization, sequence/optional handling, parse error paths, JSON prettification, YAML formatting rules, and snapshot path generation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Serializer
    participant Frame as Frame Stack
    participant Node as YAML::Node
    participant YamlCpp as yaml-cpp

    User->>Serializer: to_yaml(value)
    Serializer->>Serializer: create Serializer<Config>
    Serializer->>Serializer: serialize(value)
    Serializer->>Frame: begin_object()
    Frame->>Frame: push new frame
    loop for each field
        Serializer->>Frame: field(name)
        Serializer->>Serializer: serialize_field(value)
        Serializer->>Node: write scalar/nested
    end
    Serializer->>Frame: end_object()
    Frame->>Frame: pop frame, merge to parent
    Serializer->>Node: retrieve root node
    Node-->>User: return YAML::Node
Loading
sequenceDiagram
    participant Test as Test Execution
    participant SnapAPI as snap::check()
    participant Registry as TestContext
    participant FS as Filesystem
    participant Report as Output

    Test->>Registry: populate current_test_context()
    Test->>SnapAPI: check(content, expr, loc)
    SnapAPI->>Registry: read snap_counter
    SnapAPI->>FS: construct snapshot path
    alt snapshot file exists
        SnapAPI->>FS: read stored snapshot
        alt content matches
            SnapAPI-->>Test: return matched
        else content differs
            SnapAPI->>Report: log location + colored diff
            SnapAPI->>FS: optionally update if ZEST_UPDATE_SNAPSHOTS
            SnapAPI-->>Test: return updated/mismatch
        end
    else snapshot file missing
        SnapAPI->>FS: create new snapshot file
        SnapAPI-->>Test: return created
    end
    Registry->>Registry: increment snap_counter
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #120: Introduces codec::serializer_like and codec::deserializer_like concepts that the new YAML Serializer/Deserializer implementations depend on for compile-time interface validation.
  • PR #113: Provides shared fixture headers (e.g., tests/fixtures/schema/common.h) referenced by both the YAML codec tests and snapshot testing test suite.

Poem

🐰 Hops with joy through YAML streams
Snapshots stored in filesystem dreams
Context tracked, frame by frame
Testing made a joyful game ✨📸

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(codec): add yaml-cpp codec wrapper' clearly and concisely summarizes the main change—adding a YAML codec implementation using yaml-cpp. The title accurately reflects the primary objective shown in the changeset and PR objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/codec-yaml

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…exception builds

- Deserializer now uses YAML::convert<T>::decode() which returns bool
  instead of node.as<T>() which throws on failure
- parse_node() guarded with #if KOTA_ENABLE_EXCEPTIONS for YAML::Load()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (6)
include/kota/codec/yaml/serializer.h (2)

31-41: Error type mismatch with the deserializer.

Serializer::error_type is error_kind while Deserializer::error_type in include/kota/codec/yaml/deserializer.h (line 64) is yaml::error. The free function to_yaml (line 200) is declared to return std::expected<YAML::Node, error>, so there's an implicit conversion happening at line 204. This works as long as error is constructible from error_kind, but the asymmetry will surprise anyone composing the two backends or writing generic code over yaml::Serializer / yaml::Deserializer. Prefer aligning both to yaml::error.

🛠️ Suggested alignment
-    using error_type = error_kind;
+    using error_type = yaml::error;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/kota/codec/yaml/serializer.h` around lines 31 - 41, Serializer's
error_type is currently error_kind while Deserializer uses yaml::error, causing
implicit conversions in to_yaml and surprising asymmetry; change
Serializer::error_type (and any dependent typedefs like result_t and status_t)
from error_kind to yaml::error so both backends align, update any uses of
error_kind within serializer.h to use yaml::error (or adapt constructors if
needed), and ensure the free function to_yaml's declared return
std::expected<YAML::Node, error> (and any alias named error) matches the new
yaml::error type so no implicit conversion is required.

98-120: Defensive: field/end_object/end_array assume non-empty stack.

ser_stack.back() is UB on an empty vector. The codec streaming protocol should always pair begin/end correctly, so today this is a pure invariant, but a stray serialize_field(...) at the top level (e.g., due to a bug in a custom serialize_traits) would quietly corrupt memory rather than surface a diagnosable error. An assertion or an error_kind::invalid_state return would make the failure mode debuggable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/kota/codec/yaml/serializer.h` around lines 98 - 120, Add defensive
checks for an empty ser_stack at the start of field, end_object (and similarly
end_array) to avoid UB from ser_stack.back() on an empty vector: if
ser_stack.empty() return an error_t status indicating invalid_state (or assert)
before touching ser_stack; only call ser_stack.back(), pop_back(), or access
parent after that check. Update the functions named field(std::string_view),
end_object(), and end_array() to perform this guard and return
error_kind::invalid_state via the status_t on failure so callers get a
diagnosable error instead of silent UB.
tests/unit/zest/snap_test.cpp (1)

159-175: Minor: missing coverage for combined counter > 0 + GlobContext.

The three snapshot_path_* cases exercise counter-only or glob-only, but not both together. The implementation in src/zest/snap.cpp orders the filename as suite__test__stem@N.snap, which is worth pinning with a test so an accidental ordering swap (e.g., suite__test@N__stem.snap) doesn't regress silently.

🧪 Suggested extra case
 TEST_CASE(snapshot_path_glob) {
     snap::GlobContext glob_ctx{"input1"};
     auto path =
         snap::snapshot_path("/home/user/tests/test.cpp", "suite", "case", 0, glob_ctx);
     EXPECT_EQ(path.filename().string(), "suite__case__input1.snap");
 }
+
+TEST_CASE(snapshot_path_glob_multi) {
+    snap::GlobContext glob_ctx{"input1"};
+    auto path =
+        snap::snapshot_path("/home/user/tests/test.cpp", "suite", "case", 2, glob_ctx);
+    EXPECT_EQ(path.filename().string(), "suite__case__input1@3.snap");
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/zest/snap_test.cpp` around lines 159 - 175, Add a unit test in
tests/unit/zest/snap_test.cpp that calls snap::snapshot_path with both a
positive counter (e.g., 1) and a non-empty snap::GlobContext (e.g., {"input1"})
to assert the filename ordering is "suite__case__stem@N.snap" (i.e., glob stem
before `@N`); implement this as a new TEST_CASE (name it something like
snapshot_path_counter_and_glob) that verifies path.filename().string() equals
"suite__case__input1@2.snap" for the inputs used and reuse the existing pattern
of constructing the path and checking filename().string().
src/zest/snap.cpp (2)

399-406: Stale glob_ctx leaks to subsequent tests on exception.

If callback(path) throws, the glob_ctx.reset() at line 405 is skipped and the thread-local GlobContext remains set to the last iteration's stem. Subsequent snapshot calls outside the glob block will then be routed under that stale stem. Wrap the iteration in RAII so the reset runs unconditionally.

🛠️ Suggested RAII fix
     auto& glob_ctx = current_glob_context();
+    struct glob_guard {
+        std::optional<GlobContext>& ref;
+        ~glob_guard() { ref.reset(); }
+    } guard{glob_ctx};
     for(const auto& path : matched) {
         glob_ctx = GlobContext{path.stem().string()};
         reset_counter();
         callback(path);
     }
-    glob_ctx.reset();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zest/snap.cpp` around lines 399 - 406, The loop leaks a stale
thread-local GlobContext if callback(path) throws because glob_ctx.reset() is
skipped; wrap per-iteration context management in an RAII guard so the
GlobContext is set at start and always reset on scope exit (even on exceptions).
Replace the manual assignment/reset of current_glob_context()/glob_ctx and the
trailing glob_ctx.reset() with a small scoped helper (e.g., ScopedGlobContext or
similar) that constructs from path.stem().string() and clears the thread-local
in its destructor, and keep calls to reset_counter() and callback(path)
unchanged inside the loop so reset runs unconditionally.

186-189: Dead conditional — remove or replace with intended logic.

The if(!child_compound && i < arr.size() - 1) { } block has an empty body with only an explanatory comment. Either the guard is unused and should be dropped, or the intended separator logic was never implemented.

🧹 Cleanup
-                bool child_compound = arr[i].is_object() || arr[i].is_array();
                 emit_yaml_impl(out, arr[i], depth + 1, true);
-                if(!child_compound && i < arr.size() - 1) {
-                    // scalar items are already complete
-                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zest/snap.cpp` around lines 186 - 189, The conditional if(!child_compound
&& i < arr.size() - 1) { } is a dead/empty guard and should be fixed: either
remove this entire if statement (leaving no-op behavior) or implement the
missing separator/continuation logic for scalar array items—e.g., invoke the
serializer/emitter method used elsewhere to append a separator (comma/newline)
between items; locate usages around arr, i, and child_compound in snap.cpp (the
array serialization loop) and make the change consistently with how compound
items are handled.
include/kota/zest/snap.h (1)

93-128: Undeclared dependencies on json.h, content/serializer.h, and zest.h.

The JSON/YAML snapshot macros reference ::kota::codec::json::to_json, ::kota::codec::content::Serializer, ::kota::codec::serialize, ::kota::zest::pretty_dump, ::kota::zest::print_trace, and ::kota::zest::failure, none of which are declared by this header. Today the test files include these explicitly, but the requirement is invisible to a user who writes only #include "kota/zest/snap.h". Add a comment block near the macros listing the required headers (or include the minimally necessary ones here).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/kota/zest/snap.h` around lines 93 - 128, The header uses symbols like
::kota::codec::json::to_json, ::kota::codec::content::Serializer<>,
::kota::codec::serialize, ::kota::zest::prettify_json,
::kota::zest::value_to_yaml, ::kota::zest::snap::check,
::kota::zest::snap::Result, ::kota::zest::print_trace and ::kota::zest::failure
but does not declare or include their headers; add a short comment block above
the snapshot macros listing the required includes (or add the minimal `#include`
directives) such as the headers that declare kota/codec/json.h,
kota/codec/content/serializer.h, and kota/zest.h so any user including
kota/zest/snap.h gets the needed declarations for EXPECT_JSON_SNAPSHOT,
ASSERT_JSON_SNAPSHOT, EXPECT_YAML_SNAPSHOT and ASSERT_YAML_SNAPSHOT to compile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@include/kota/codec/yaml/deserializer.h`:
- Around line 290-310: The next_field() function ignores the boolean result of
YAML::convert<std::string>::decode, so a failed key decode will leave
pending_key stale and return an empty field name; update next_field() to check
the decode return value (call to
YAML::convert<std::string>::decode(frame.iter->first, frame.pending_key)) and if
it returns false propagate an error via mark_invalid (e.g.
mark_invalid(error_kind::decode_error) or an appropriate error_kind), ensuring
you do not set frame.pending_valid or current_value before returning the error;
keep the rest of the iterator advancement logic intact so pending_valid is only
set on a successful decode.
- Around line 162-189: deserialize_uint currently decodes the scalar into
std::int64_t which loses values > INT64_MAX; change the reader lambda inside
deserialize_uint/read_scalar to decode into std::uint64_t (use
YAML::convert<std::uint64_t>::decode) and then reject negative values by
checking the node or decoded value as needed before narrowing; keep the
subsequent cast to uint64_t/narrow_uint<T> and error mappings
(mark_invalid(error_kind::number_out_of_range) or type_mismatch) intact so large
unsigned values in (INT64_MAX, UINT64_MAX] are accepted and still validated by
codec::detail::narrow_uint, and ensure any type-mismatch for negative scalars is
preserved.

In `@include/kota/codec/yaml/yaml.h`:
- Around line 17-27: The no-exceptions branch of parse_node currently calls
YAML::Load(std::string(text)) unguarded so if yaml-cpp was built with exceptions
enabled a ParserException will still be thrown (leading to terminate); either
document that yaml-cpp must be built with exceptions disabled (e.g.,
YAML_CPP_NO_EXCEPTIONS) when KOTA_ENABLE_EXCEPTIONS==0, or replace the
no-exceptions path with explicit error-handling using yaml-cpp's low-level
Parser and a custom EventHandler to detect parse failures and return
std::unexpected(error_kind::parse_error); update parse_node, mention YAML::Load
and error_kind::parse_error, and gate behavior on KOTA_ENABLE_EXCEPTIONS so
behavior is well-defined.
- Around line 48-58: The to_string<T> template currently returns the Emitter
buffer even when YAML::Emitter failed; update the function (the to_string
template that calls to_yaml and uses YAML::Emitter) to call emitter.good() after
emitting and before returning, and if !emitter.good() return std::unexpected
with an error constructed using an appropriate kind (e.g.,
error_kind::invalid_state) and the emitter.GetLastError() or emitter.c_str() as
the message; keep the existing to_yaml call and error path for node creation
unchanged.

In `@include/kota/zest/snap.h`:
- Around line 111-125: The macro ZEST_YAML_SNAPSHOT_CHECK wraps __VA_ARGS__ in
parentheses, causing a comma-expression and silently dropping earlier arguments
(e.g., codec::serialize(_zest_yaml_ser_, (__VA_ARGS__)) makes (a, b) evaluate to
b); remove the parentheses so the macro forwards __VA_ARGS__ directly like
ZEST_SNAPSHOT_CHECK and ZEST_JSON_SNAPSHOT_CHECK (i.e., use
codec::serialize(_zest_yaml_ser_, __VA_ARGS__)) so multi-argument calls fail to
compile rather than silently collapse — alternatively implement an
__VA_OPT__/helper check to explicitly reject multiple arguments if you prefer an
explicit diagnostic.

In `@src/codec/CMakeLists.txt`:
- Around line 127-163: When KOTA_CODEC_ENABLE_YAML is enabled but
KOTA_ENABLE_EXCEPTIONS is OFF, yaml-cpp will break builds; update the CMake
logic so the kota_codec_yaml target (kota::codec::yaml) and its
target_link_libraries/target_include_directories are only created when
KOTA_ENABLE_EXCEPTIONS is ON, otherwise emit a clear CMake error/message that
the YAML backend requires exceptions; additionally, adjust
include/kota/codec/yaml/yaml.h to avoid calling YAML::Load or including yaml-cpp
headers in the no-exceptions branch (remove or guard the YAML::Load invocation)
so no translation unit pulls in yaml-cpp when exceptions are disabled.

In `@src/zest/snap.cpp`:
- Around line 128-143: The float-emission branch (case ValueKind::floating)
currently uses val.as_double() into d and std::to_string(d) — this emits
"inf"/"-inf"/"nan" and loses precision; update this block to first test
non-finite values with std::isfinite(d) and emit YAML-compliant strings ".inf",
"-.inf", or ".nan" for +inf/-inf/NaN respectively (use val/as_double() variable
d), and for finite values replace std::to_string(d) with a round-trip-safe
formatter (std::to_chars or std::format/ostringstream with sufficient precision)
so you can still perform the trailing-zero trimming logic on the resulting
string (refer to variables s, dot, last_nonzero used in the existing code).

---

Nitpick comments:
In `@include/kota/codec/yaml/serializer.h`:
- Around line 31-41: Serializer's error_type is currently error_kind while
Deserializer uses yaml::error, causing implicit conversions in to_yaml and
surprising asymmetry; change Serializer::error_type (and any dependent typedefs
like result_t and status_t) from error_kind to yaml::error so both backends
align, update any uses of error_kind within serializer.h to use yaml::error (or
adapt constructors if needed), and ensure the free function to_yaml's declared
return std::expected<YAML::Node, error> (and any alias named error) matches the
new yaml::error type so no implicit conversion is required.
- Around line 98-120: Add defensive checks for an empty ser_stack at the start
of field, end_object (and similarly end_array) to avoid UB from ser_stack.back()
on an empty vector: if ser_stack.empty() return an error_t status indicating
invalid_state (or assert) before touching ser_stack; only call ser_stack.back(),
pop_back(), or access parent after that check. Update the functions named
field(std::string_view), end_object(), and end_array() to perform this guard and
return error_kind::invalid_state via the status_t on failure so callers get a
diagnosable error instead of silent UB.

In `@include/kota/zest/snap.h`:
- Around line 93-128: The header uses symbols like ::kota::codec::json::to_json,
::kota::codec::content::Serializer<>, ::kota::codec::serialize,
::kota::zest::prettify_json, ::kota::zest::value_to_yaml,
::kota::zest::snap::check, ::kota::zest::snap::Result, ::kota::zest::print_trace
and ::kota::zest::failure but does not declare or include their headers; add a
short comment block above the snapshot macros listing the required includes (or
add the minimal `#include` directives) such as the headers that declare
kota/codec/json.h, kota/codec/content/serializer.h, and kota/zest.h so any user
including kota/zest/snap.h gets the needed declarations for
EXPECT_JSON_SNAPSHOT, ASSERT_JSON_SNAPSHOT, EXPECT_YAML_SNAPSHOT and
ASSERT_YAML_SNAPSHOT to compile.

In `@src/zest/snap.cpp`:
- Around line 399-406: The loop leaks a stale thread-local GlobContext if
callback(path) throws because glob_ctx.reset() is skipped; wrap per-iteration
context management in an RAII guard so the GlobContext is set at start and
always reset on scope exit (even on exceptions). Replace the manual
assignment/reset of current_glob_context()/glob_ctx and the trailing
glob_ctx.reset() with a small scoped helper (e.g., ScopedGlobContext or similar)
that constructs from path.stem().string() and clears the thread-local in its
destructor, and keep calls to reset_counter() and callback(path) unchanged
inside the loop so reset runs unconditionally.
- Around line 186-189: The conditional if(!child_compound && i < arr.size() - 1)
{ } is a dead/empty guard and should be fixed: either remove this entire if
statement (leaving no-op behavior) or implement the missing
separator/continuation logic for scalar array items—e.g., invoke the
serializer/emitter method used elsewhere to append a separator (comma/newline)
between items; locate usages around arr, i, and child_compound in snap.cpp (the
array serialization loop) and make the change consistently with how compound
items are handled.

In `@tests/unit/zest/snap_test.cpp`:
- Around line 159-175: Add a unit test in tests/unit/zest/snap_test.cpp that
calls snap::snapshot_path with both a positive counter (e.g., 1) and a non-empty
snap::GlobContext (e.g., {"input1"}) to assert the filename ordering is
"suite__case__stem@N.snap" (i.e., glob stem before `@N`); implement this as a new
TEST_CASE (name it something like snapshot_path_counter_and_glob) that verifies
path.filename().string() equals "suite__case__input1@2.snap" for the inputs used
and reuse the existing pattern of constructing the path and checking
filename().string().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bfa7ec7d-5337-40f7-9d5a-8071794cac08

📥 Commits

Reviewing files that changed from the base of the PR and between 42e62d1 and 3d0825b.

⛔ Files ignored due to path filters (8)
  • tests/unit/zest/__snapshots__/zest_snap__snapshot_json_struct.snap is excluded by !**/*.snap
  • tests/unit/zest/__snapshots__/zest_snap__snapshot_json_vector.snap is excluded by !**/*.snap
  • tests/unit/zest/__snapshots__/zest_snap__snapshot_multiple_per_test.snap is excluded by !**/*.snap
  • tests/unit/zest/__snapshots__/zest_snap__snapshot_multiple_per_test@2.snap is excluded by !**/*.snap
  • tests/unit/zest/__snapshots__/zest_snap__snapshot_multiple_per_test@3.snap is excluded by !**/*.snap
  • tests/unit/zest/__snapshots__/zest_snap__snapshot_scalar.snap is excluded by !**/*.snap
  • tests/unit/zest/__snapshots__/zest_snap__snapshot_scalar@2.snap is excluded by !**/*.snap
  • tests/unit/zest/__snapshots__/zest_snap__snapshot_yaml_struct.snap is excluded by !**/*.snap
📒 Files selected for processing (14)
  • CMakeLists.txt
  • include/kota/codec/yaml/deserializer.h
  • include/kota/codec/yaml/error.h
  • include/kota/codec/yaml/serializer.h
  • include/kota/codec/yaml/yaml.h
  • include/kota/zest/detail/registry.h
  • include/kota/zest/detail/suite.h
  • include/kota/zest/snap.h
  • src/codec/CMakeLists.txt
  • src/zest/CMakeLists.txt
  • src/zest/snap.cpp
  • tests/CMakeLists.txt
  • tests/unit/codec/yaml/yaml_tests.cpp
  • tests/unit/zest/snap_test.cpp

Comment on lines +162 to +189
template <codec::uint_like T>
status_t deserialize_uint(T& value) {
std::int64_t parsed = 0;
auto status = read_scalar(parsed, [](const YAML::Node& node) -> result_t<std::int64_t> {
std::int64_t v{};
if(!YAML::convert<std::int64_t>::decode(node, v)) {
return std::unexpected(error_kind::type_mismatch);
}
return v;
});
if(!status) {
return std::unexpected(status.error());
}

if(parsed < 0) {
return mark_invalid(error_kind::number_out_of_range);
}

const auto unsigned_value = static_cast<std::uint64_t>(parsed);
auto narrowed =
codec::detail::narrow_uint<T>(unsigned_value, error_kind::number_out_of_range);
if(!narrowed) {
return mark_invalid(narrowed.error());
}

value = *narrowed;
return {};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does yaml-cpp YAML::convert<std::uint64_t>::decode reject negative scalar strings?

💡 Result:

Yes, yaml-cpp's YAML::convertstd::uint64_t::decode rejects negative scalar strings by returning false, which causes node.asstd::uint64_t to throw YAML::TypedBadConversion.

Citations:


deserialize_uint loses values above INT64_MAX.

Decoding through std::int64_t first causes the method to reject unsigned integers in the range (INT64_MAX, UINT64_MAX]. For example, deserializing 18446744073709551615 (max uint64_t) into a std::uint64_t field fails with error_kind::type_mismatch instead of returning the correct value. Decode directly into std::uint64_t, which safely rejects negative scalars during the conversion.

🛠️ Suggested fix
     template <codec::uint_like T>
     status_t deserialize_uint(T& value) {
-        std::int64_t parsed = 0;
-        auto status = read_scalar(parsed, [](const YAML::Node& node) -> result_t<std::int64_t> {
-            std::int64_t v{};
-            if(!YAML::convert<std::int64_t>::decode(node, v)) {
+        std::uint64_t parsed = 0;
+        auto status = read_scalar(parsed, [](const YAML::Node& node) -> result_t<std::uint64_t> {
+            std::uint64_t v{};
+            if(!YAML::convert<std::uint64_t>::decode(node, v)) {
                 return std::unexpected(error_kind::type_mismatch);
             }
             return v;
         });
         if(!status) {
             return std::unexpected(status.error());
         }
 
-        if(parsed < 0) {
-            return mark_invalid(error_kind::number_out_of_range);
-        }
-
-        const auto unsigned_value = static_cast<std::uint64_t>(parsed);
         auto narrowed =
-            codec::detail::narrow_uint<T>(unsigned_value, error_kind::number_out_of_range);
+            codec::detail::narrow_uint<T>(parsed, error_kind::number_out_of_range);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/kota/codec/yaml/deserializer.h` around lines 162 - 189,
deserialize_uint currently decodes the scalar into std::int64_t which loses
values > INT64_MAX; change the reader lambda inside deserialize_uint/read_scalar
to decode into std::uint64_t (use YAML::convert<std::uint64_t>::decode) and then
reject negative values by checking the node or decoded value as needed before
narrowing; keep the subsequent cast to uint64_t/narrow_uint<T> and error
mappings (mark_invalid(error_kind::number_out_of_range) or type_mismatch) intact
so large unsigned values in (INT64_MAX, UINT64_MAX] are accepted and still
validated by codec::detail::narrow_uint, and ensure any type-mismatch for
negative scalars is preserved.

Comment on lines +290 to +310
result_t<std::optional<std::string_view>> next_field() {
if(!is_valid || deser_stack.empty()) {
return mark_invalid(error_kind::invalid_state);
}
auto& frame = deser_stack.back();

if(frame.pending_valid) {
++frame.iter;
frame.pending_valid = false;
}

if(frame.iter == frame.end_iter) {
current_value.clear();
return std::optional<std::string_view>(std::nullopt);
}

YAML::convert<std::string>::decode(frame.iter->first, frame.pending_key);
frame.pending_valid = true;
current_value.set(frame.iter->second);
return std::optional<std::string_view>(frame.pending_key);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unchecked YAML::convert<std::string>::decode in next_field.

The return value is ignored, so if key decoding ever fails (non-scalar key, unusual node state), pending_key is left in its prior/empty state and the caller receives an empty string_view as the field name without an error being reported. Check the return and propagate a real error.

🛠️ Suggested fix
-        YAML::convert<std::string>::decode(frame.iter->first, frame.pending_key);
+        if(!YAML::convert<std::string>::decode(frame.iter->first, frame.pending_key)) {
+            return mark_invalid(error_kind::type_mismatch);
+        }
         frame.pending_valid = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
result_t<std::optional<std::string_view>> next_field() {
if(!is_valid || deser_stack.empty()) {
return mark_invalid(error_kind::invalid_state);
}
auto& frame = deser_stack.back();
if(frame.pending_valid) {
++frame.iter;
frame.pending_valid = false;
}
if(frame.iter == frame.end_iter) {
current_value.clear();
return std::optional<std::string_view>(std::nullopt);
}
YAML::convert<std::string>::decode(frame.iter->first, frame.pending_key);
frame.pending_valid = true;
current_value.set(frame.iter->second);
return std::optional<std::string_view>(frame.pending_key);
}
result_t<std::optional<std::string_view>> next_field() {
if(!is_valid || deser_stack.empty()) {
return mark_invalid(error_kind::invalid_state);
}
auto& frame = deser_stack.back();
if(frame.pending_valid) {
+frame.iter;
frame.pending_valid = false;
}
if(frame.iter == frame.end_iter) {
current_value.clear();
return std::optional<std::string_view>(std::nullopt);
}
if(!YAML::convert<std::string>::decode(frame.iter->first, frame.pending_key)) {
return mark_invalid(error_kind::type_mismatch);
}
frame.pending_valid = true;
current_value.set(frame.iter->second);
return std::optional<std::string_view>(frame.pending_key);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/kota/codec/yaml/deserializer.h` around lines 290 - 310, The
next_field() function ignores the boolean result of
YAML::convert<std::string>::decode, so a failed key decode will leave
pending_key stale and return an empty field name; update next_field() to check
the decode return value (call to
YAML::convert<std::string>::decode(frame.iter->first, frame.pending_key)) and if
it returns false propagate an error via mark_invalid (e.g.
mark_invalid(error_kind::decode_error) or an appropriate error_kind), ensuring
you do not set frame.pending_valid or current_value before returning the error;
keep the rest of the iterator advancement logic intact so pending_valid is only
set on a successful decode.

Comment on lines +17 to +27
inline auto parse_node(std::string_view text) -> std::expected<YAML::Node, error> {
#if KOTA_ENABLE_EXCEPTIONS
try {
return YAML::Load(std::string(text));
} catch(const YAML::ParserException&) {
return std::unexpected(error_kind::parse_error);
}
#else
return YAML::Load(std::string(text));
#endif
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does yaml-cpp 0.9.0 provide a YAML::Load variant that reports parse errors without throwing exceptions?

💡 Result:

No, yaml-cpp 0.9.0 does not provide a YAML::Load variant that reports parse errors without throwing exceptions. All YAML::Load and YAML::LoadFile functions explicitly throw ParserException on malformed input, as documented in the header file. The low-level YAML::Parser class's HandleNextDocument requires an EventHandler and does not directly produce Nodes without potential exceptions internally. Open GitHub issues confirm no support for exceptionless parsing, even when exceptions are compiler-disabled; throws are hardcoded. The 0.9.0 release notes list bugfixes but no API changes for non-throwing parse error reporting.

Citations:


No-exceptions branch will abort if yaml-cpp is built with exceptions enabled.

When KOTA_ENABLE_EXCEPTIONS is 0, the code calls YAML::Load(...) unguarded. yaml-cpp 0.9.0 has no variant that reports parse errors without throwing exceptions—all YAML::Load and YAML::LoadFile functions explicitly throw YAML::ParserException on malformed input. Therefore, unless yaml-cpp itself has been built without exceptions (e.g., with YAML_CPP_NO_EXCEPTIONS), malformed input will trigger std::terminate instead of returning error_kind::parse_error.

Document the requirement that yaml-cpp must also be built without exceptions, or implement custom error handling using yaml-cpp's low-level Parser class with a custom EventHandler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/kota/codec/yaml/yaml.h` around lines 17 - 27, The no-exceptions
branch of parse_node currently calls YAML::Load(std::string(text)) unguarded so
if yaml-cpp was built with exceptions enabled a ParserException will still be
thrown (leading to terminate); either document that yaml-cpp must be built with
exceptions disabled (e.g., YAML_CPP_NO_EXCEPTIONS) when
KOTA_ENABLE_EXCEPTIONS==0, or replace the no-exceptions path with explicit
error-handling using yaml-cpp's low-level Parser and a custom EventHandler to
detect parse failures and return std::unexpected(error_kind::parse_error);
update parse_node, mention YAML::Load and error_kind::parse_error, and gate
behavior on KOTA_ENABLE_EXCEPTIONS so behavior is well-defined.

Comment on lines +48 to +58
template <typename T>
auto to_string(const T& value) -> std::expected<std::string, error> {
auto node = to_yaml(value);
if(!node) {
return std::unexpected(node.error());
}

YAML::Emitter emitter;
emitter << *node;
return std::string(emitter.c_str());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does YAML::Emitter::c_str() return nullptr on error in yaml-cpp 0.9.0?

💡 Result:

No, YAML::Emitter::c_str does not return nullptr on error in yaml-cpp 0.9.0. It always returns the const char* to the internal stringstream's content via m_stream.str, even when !good. Check good or GetLastError to detect errors.

Citations:


🏁 Script executed:

# Search for yaml-cpp in the repository to find version info and understand the code context
fd -t f "yaml" | head -20
git ls-files | grep -i yaml | head -20

Repository: clice-io/kotatsu

Length of output: 438


🏁 Script executed:

# Check the yaml.h file to see the full context and check for any error handling patterns
cat -n include/kota/codec/yaml/yaml.h | head -100

Repository: clice-io/kotatsu

Length of output: 2635


🏁 Script executed:

# Check the error.h file to understand available error kinds
cat include/kota/codec/yaml/error.h

Repository: clice-io/kotatsu

Length of output: 1012


Check emitter.good() to prevent silently returning corrupted YAML output.

YAML::Emitter::c_str() does not return nullptr on error; instead, it returns a pointer to the internal buffer even when the emitter has failed. This means a serialization error (invalid node structure, etc.) would silently produce partial or corrupted YAML rather than propagating the error to the caller. Call emitter.good() before returning to surface the failure properly.

Note: error_kind::serialize_error does not exist; use an appropriate error kind like error_kind::invalid_state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/kota/codec/yaml/yaml.h` around lines 48 - 58, The to_string<T>
template currently returns the Emitter buffer even when YAML::Emitter failed;
update the function (the to_string template that calls to_yaml and uses
YAML::Emitter) to call emitter.good() after emitting and before returning, and
if !emitter.good() return std::unexpected with an error constructed using an
appropriate kind (e.g., error_kind::invalid_state) and the
emitter.GetLastError() or emitter.c_str() as the message; keep the existing
to_yaml call and error path for node creation unchanged.

Comment thread include/kota/zest/snap.h
Comment on lines +111 to +125
#define ZEST_YAML_SNAPSHOT_CHECK(return_action, ...) \
do { \
::kota::codec::content::Serializer<> _zest_yaml_ser_; \
auto _zest_yaml_val_ = ::kota::codec::serialize(_zest_yaml_ser_, (__VA_ARGS__)); \
auto _zest_snap_str_ = _zest_yaml_val_ \
? ::kota::zest::snap::value_to_yaml(*_zest_yaml_val_) \
: std::string("<yaml serialization failed>"); \
auto _zest_snap_result_ = ::kota::zest::snap::check( \
_zest_snap_str_, #__VA_ARGS__, std::source_location::current()); \
if(_zest_snap_result_ == ::kota::zest::snap::Result::mismatch) { \
::kota::zest::print_trace(std::source_location::current()); \
::kota::zest::failure(); \
return_action; \
} \
} while(0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Parenthesizing (__VA_ARGS__) creates a comma-expression for multi-arg calls.

Unlike ZEST_SNAPSHOT_CHECK and ZEST_JSON_SNAPSHOT_CHECK which forward __VA_ARGS__ directly, this macro wraps it: (__VA_ARGS__). If a caller writes EXPECT_YAML_SNAPSHOT(a, b), the expansion becomes codec::serialize(_zest_yaml_ser_, (a, b)), and the parenthesized (a, b) collapses to the second operand via the comma operator — silently dropping a. Either strip the parentheses (to get the same "single-value only" semantics as the other macros via a compile error on multi-arg calls) or switch to __VA_OPT__/helper to reject multi-arg usage explicitly.

🛠️ Suggested fix
-        auto _zest_yaml_val_ = ::kota::codec::serialize(_zest_yaml_ser_, (__VA_ARGS__));            \
+        auto _zest_yaml_val_ = ::kota::codec::serialize(_zest_yaml_ser_, __VA_ARGS__);              \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/kota/zest/snap.h` around lines 111 - 125, The macro
ZEST_YAML_SNAPSHOT_CHECK wraps __VA_ARGS__ in parentheses, causing a
comma-expression and silently dropping earlier arguments (e.g.,
codec::serialize(_zest_yaml_ser_, (__VA_ARGS__)) makes (a, b) evaluate to b);
remove the parentheses so the macro forwards __VA_ARGS__ directly like
ZEST_SNAPSHOT_CHECK and ZEST_JSON_SNAPSHOT_CHECK (i.e., use
codec::serialize(_zest_yaml_ser_, __VA_ARGS__)) so multi-argument calls fail to
compile rather than silently collapse — alternatively implement an
__VA_OPT__/helper check to explicitly reject multiple arguments if you prefer an
explicit diagnostic.

Comment thread src/codec/CMakeLists.txt
Comment on lines +127 to +163
if(KOTA_CODEC_ENABLE_YAML)
set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
set(YAML_CPP_INSTALL OFF CACHE BOOL "" FORCE)
set(YAML_CPP_FORMAT_SOURCE OFF CACHE BOOL "" FORCE)

kota_add_git_dependency(
yaml-cpp
GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git
GIT_TAG yaml-cpp-0.9.0
OPTIONS
YAML_CPP_BUILD_TESTS OFF
YAML_CPP_BUILD_TOOLS OFF
YAML_CPP_BUILD_CONTRIB OFF
YAML_CPP_INSTALL OFF
YAML_CPP_FORMAT_SOURCE OFF
)

add_library(kota_codec_yaml INTERFACE)
add_library(kota::codec::yaml ALIAS kota_codec_yaml)

target_include_directories(kota_codec_yaml INTERFACE
"${PROJECT_SOURCE_DIR}/include"
)

target_link_libraries(kota_codec_yaml INTERFACE
kota::meta
yaml-cpp::yaml-cpp
)

kota_apply_project_options(kota_codec_yaml)

target_link_libraries(kota_codec INTERFACE
kota::codec::yaml
)
endif()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether yaml-cpp headers use try/catch that would be incompatible with -fno-exceptions in consuming TUs.
rg -n --type=cpp -g '!**/test/**' -C1 '\b(try|catch)\s*[({]' 2>/dev/null | head -50 || true

# Check how the codec header uses YAML and whether exception-bearing APIs are reachable when KOTA_ENABLE_EXCEPTIONS=0.
fd -t f 'yaml\.h$' include/kota/codec/yaml | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
fd -t f 'deserializer\.h$' include/kota/codec/yaml | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'

Repository: clice-io/kotatsu

Length of output: 20839


🌐 Web query:

Does yaml-cpp 0.9.0 support being built or consumed with -fno-exceptions (equivalent of TOML_EXCEPTIONS=0 / SIMDJSON_EXCEPTIONS=0)?

💡 Result:

No, yaml-cpp 0.9.0 does not support being built or consumed with -fno-exceptions. The library uses throw statements directly in its source code without conditional compilation to disable exceptions, causing compilation failures when exceptions are disabled. Multiple open GitHub issues (#799, #930, #196) request this support, and an unmerged PR (#1146) proposes a solution using a YAML_throw macro that calls a user-provided handler when exceptions are disabled. The 0.9.0 release notes list bugfixes and CMake updates but no mention of exceptions support. Stack Overflow confirms exceptions are hardcoded for version 0.8.0, and no evidence indicates changes in 0.9.0. Users must compile with exceptions enabled (-fexceptions).

Citations:


yaml-cpp has no support for building or consuming with exceptions disabled; restrict header exposure or document platform incompatibility.

The yaml-cpp 0.9.0 library unconditionally uses throw statements without conditional compilation to disable exceptions. Unlike the TOML and simdjson backends (which forward TOML_EXCEPTIONS and SIMDJSON_EXCEPTIONS based on KOTA_ENABLE_EXCEPTIONS), yaml-cpp provides no equivalent mechanism.

When KOTA_ENABLE_EXCEPTIONS=OFF and the downstream project compiles with -fno-exceptions, including yaml-cpp/yaml.h (directly in kota::codec::yaml::Deserializer header) will fail to compile because the library's headers contain try-catch blocks that are incompatible with -fno-exceptions.

Additionally, the current code at include/kota/codec/yaml/yaml.h lines 23–24 still invokes YAML::Load() in the no-exceptions branch, which relies on yaml-cpp's exception-based error reporting without a mechanism to catch or handle those exceptions.

Either document that the YAML codec backend is incompatible with KOTA_ENABLE_EXCEPTIONS=OFF and prevent the target from being built in that configuration, or refactor to delay yaml-cpp header inclusion until exception handling is confirmed (e.g., via a runtime check or separate TU compiled with -fexceptions).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/codec/CMakeLists.txt` around lines 127 - 163, When KOTA_CODEC_ENABLE_YAML
is enabled but KOTA_ENABLE_EXCEPTIONS is OFF, yaml-cpp will break builds; update
the CMake logic so the kota_codec_yaml target (kota::codec::yaml) and its
target_link_libraries/target_include_directories are only created when
KOTA_ENABLE_EXCEPTIONS is ON, otherwise emit a clear CMake error/message that
the YAML backend requires exceptions; additionally, adjust
include/kota/codec/yaml/yaml.h to avoid calling YAML::Load or including yaml-cpp
headers in the no-exceptions branch (remove or guard the YAML::Load invocation)
so no translation unit pulls in yaml-cpp when exceptions are disabled.

Comment thread src/zest/snap.cpp
Comment on lines +128 to +143
case ValueKind::floating: {
auto d = val.as_double();
auto s = std::to_string(d);
// trim trailing zeros but keep at least one decimal
auto dot = s.find('.');
if(dot != std::string::npos) {
auto last_nonzero = s.find_last_not_of('0');
if(last_nonzero != std::string::npos && last_nonzero > dot) {
s.erase(last_nonzero + 1);
} else {
s.erase(dot + 2);
}
}
out += s;
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Float emission mishandles inf/-inf/nan.

std::to_string(±inf) returns "inf"/"-inf" and NaN returns "nan" — none of them contain . so they bypass the trailing-zero trim and get emitted verbatim. YAML spec expects .inf / -.inf / .nan for these special values, so a snapshot containing a non-finite double will be non-standard YAML. Also worth noting: std::to_string(double) uses %f with 6-digit precision, which loses precision for very small/large values compared to std::format("{}", d) or std::to_chars round-trip formatting.

🧰 Tools
🪛 Cppcheck (2.20.0)

[error] 137-137: Syntax Error

(internalAstError)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zest/snap.cpp` around lines 128 - 143, The float-emission branch (case
ValueKind::floating) currently uses val.as_double() into d and std::to_string(d)
— this emits "inf"/"-inf"/"nan" and loses precision; update this block to first
test non-finite values with std::isfinite(d) and emit YAML-compliant strings
".inf", "-.inf", or ".nan" for +inf/-inf/NaN respectively (use val/as_double()
variable d), and for finite values replace std::to_string(d) with a
round-trip-safe formatter (std::to_chars or std::format/ostringstream with
sufficient precision) so you can still perform the trailing-zero trimming logic
on the resulting string (refer to variables s, dot, last_nonzero used in the
existing code).

@16bit-ykiko
16bit-ykiko deleted the feat/codec-yaml branch May 17, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant