Skip to content

✨ Add a represent emitter protocol to dumps#247

Open
frenck wants to merge 18 commits into
mainfrom
frenck/represent-protocol
Open

✨ Add a represent emitter protocol to dumps#247
frenck wants to merge 18 commits into
mainfrom
frenck/represent-protocol

Conversation

@frenck

@frenck frenck commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Breaking change

None. represent= is a new optional keyword; omit it and dumps behaves exactly as before, on the same fast path. All existing options, default=, and serializers= are unchanged.

Proposed change

dumps could shape unknown types (serializers= maps a type to a custom !tag, default= fires for types nothing else handles), but it could not control how a builtin renders: mask a str, format a float a specific way, force a scalar's style, or emit a specific tag. PyYAML can, because its representers dispatch on any type and return a node with an explicit tag and style. That gap is the one thing keeping import yaml in downstreams that ship a hand-rolled Dumper (the driver is ESPHome, whose parser already moved to yamlrocks).

This adds a represent= callback the emitter invokes for every value it is about to emit, builtins included. It returns one of three node descriptors, or None to defer to the built-in rendering:

yamlrocks.YAMLRocksScalar(value, *, tag=None, style="auto")
yamlrocks.YAMLRocksSequence(items, *, tag=None, flow=None)
yamlrocks.YAMLRocksMapping(pairs, *, tag=None, flow=None)

The descriptors carry the original host objects, not pre-rendered nodes, so the emitter drives the recursion and keeps owning indentation, flow, sort_keys, and anchor detection; the host only ever describes one level. The path lowers to the round-trip node tree and emits through the round-trip emitter (which already speaks per-node style, tags, and flow) rather than the fast Value emitter, so plain dumps is untouched (see adr/021).

The represent path reproduces PyYAML's dump behavior so a host's representers port verbatim, with auto style and their existing tags:

  • block sequences indent under their key,
  • OPT_SORT_KEYS applies,
  • a standard tag the value already resolves to (!!bool, !!float, !!str) is elided, while a custom tag (!extend) is kept and force-quoted (a plain form would lose the tag),
  • multi-line strings default to a | literal block,
  • a shared object emits once with an anchor and aliases the repeats (cycles resolve to an alias), matching PyYAML's shared-reference behavior.

Deferred values (where represent returns None) render byte-for-byte like a plain dumps.

Line-wrap (width) composition on the represent path is intentionally deferred; it is the lowest-priority item for the reference consumer.

Type of change

  • Dependency or tooling upgrade
  • Bugfix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Deprecation (replaces or removes a feature, with a migration path)
  • Breaking change (a fix or feature that changes existing behavior)
  • Code quality, refactor, or test-only change
  • Documentation only

Additional information

  • This PR fixes or closes issue: None
  • This PR is related to: None
  • Link to a separate documentation pull request: None (docs are included, guides/dumping.md)

Checklist

  • I have read the AI Policy, and this pull request was not created by an autonomous agent.
  • I fully understand the code in this pull request and can explain every line, including any AI-assisted changes.
  • The change is covered by tests, and uv run pytest passes locally. A pull request cannot be merged unless CI is green.
  • uv run ruff check . and uv run ruff format --check . pass.
  • cargo fmt --check and cargo clippy --all-targets -- -D warnings pass.
  • Round-trip fidelity is preserved: an unmodified document still re-emits byte-for-byte.
  • No commented-out or dead code is left in the pull request.

If the change is user-facing:

  • Documentation under docs/ is added or updated, and docs/verify_examples.py still passes.

Copilot AI review requested due to automatic review settings July 9, 2026 17:53
@frenck frenck added the new-feature New features or options. label Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a represent= parameter to dumps, allowing callbacks to return scalar, sequence, or mapping descriptors for custom YAML output, or None to defer to built-in handling. It adds recursive lowering with tags, styles, sorting, anchors, aliases, and cycle support, then emits through a round-trip dump path. Public exports, type stubs, documentation, an ADR, shared literal-block logic, and comprehensive tests are included.

Possibly related PRs

  • frenck/YAMLRocks#223: Modifies the same yamlrocks.dumps PyO3 entrypoint while changing another dump keyword parameter.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a represent protocol to dumps.
Description check ✅ Passed The description is detailed and directly matches the changeset, including the new represent callback and its behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

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.

@codspeed-hq

codspeed-hq Bot commented Jul 9, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 5.07%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 15 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_dumps[medium] 102.6 µs 108.1 µs -5.07%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing frenck/represent-protocol (3c1d8df) with main (0191dd4)

Open in CodSpeed

@frenck frenck force-pushed the frenck/represent-protocol branch from 8adeb78 to 1eb0287 Compare July 9, 2026 17:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/roundtrip/value.rs (1)

544-548: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add rustdoc now that this helper is crate-public.

assigned_string_style was widened to pub(crate), but it only has internal comments. Add a short /// summary above the function. As per coding guidelines, src/**/*.rs: “Keep public Rust items documented with /// rustdoc”.

Proposed fix
+/// Choose the scalar style that preserves a string under the active schema.
 pub(crate) fn assigned_string_style(

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3e082f93-73aa-42b0-ba4d-7fc7f76e85ef

📥 Commits

Reviewing files that changed from the base of the PR and between d7b96c7 and 8adeb78.

📒 Files selected for processing (13)
  • adr/021-emitter-represent-protocol-for-dumps.md
  • adr/README.md
  • docs/src/content/docs/guides/dumping.md
  • pysrc/yamlrocks/__init__.py
  • pysrc/yamlrocks/__init__.pyi
  • src/emit_util.rs
  • src/encode/mod.rs
  • src/ffi/mod.rs
  • src/ffi/represent.rs
  • src/lib.rs
  • src/roundtrip/emit.rs
  • src/roundtrip/value.rs
  • tests/features/test_represent.py

Comment thread src/ffi/represent.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an optional represent= callback to dumps, giving hosts full, per-value control over how any value (builtins included) is emitted — an explicit tag, a scalar style, or block/flow layout — closing the gap that keeps downstreams (notably ESPHome's dumper) on import yaml. The callback returns one of three new node descriptors (YAMLRocksScalar/YAMLRocksSequence/YAMLRocksMapping) carrying the original host objects, or None to defer to the built-in rendering. When represent= is present, dumps lowers the object into a synthetic round-trip YamlNode tree and emits through the round-trip emitter (which already speaks per-node style/tag/flow), leaving the plain-dumps fast path untouched. Shared objects become anchors/aliases, OPT_SORT_KEYS applies, and deferred values are pinned byte-for-byte to a plain dumps.

Changes:

  • New src/ffi/represent.rs lowering (descriptors → YamlNode tree, PyYAML-faithful auto styling, id-based anchor detection) plus represent= wired into dumps.
  • Round-trip emitter gains a DumpConfig (sort keys, indent synthetic sequences) and an emit_roundtrip_dump entry point; use_literal_block is hoisted to shared emit_util, and assigned_string_style is made pub(crate).
  • Public surface: three new PyO3 classes exported from Rust/__init__/stubs, plus docs (guides/dumping.md) and ADR-021.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ffi/represent.rs New descriptors and lowering; error message names nonexistent classes; name_anchors recurses without a stack guard
src/ffi/mod.rs Adds represent= param and the represent branch in dumps
src/roundtrip/emit.rs DumpConfig, emit_roundtrip_dump, key sorting, synthetic-sequence indent
src/roundtrip/value.rs Exposes assigned_string_style as pub(crate)
src/emit_util.rs Shared use_literal_block for fast and represent paths
src/encode/mod.rs Delegates use_literal_block to the shared helper
src/lib.rs Registers the three new pyclasses
pysrc/yamlrocks/__init__.py / .pyi Re-exports and typing for descriptors and represent=
docs/.../guides/dumping.md New represent guide; overstates which options apply on the path
adr/021-...md, adr/README.md Records the design rationale
tests/features/test_represent.py Behavioral coverage for the new protocol

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs
Comment thread docs/src/content/docs/guides/dumping.md Outdated
Copilot AI review requested due to automatic review settings July 9, 2026 18:01
`dumps` could shape unknown types (`serializers=`, `default=`) but not control
how a builtin renders. `represent=` is a callback the emitter invokes for every
value it is about to emit, builtins included. It returns a node descriptor
(`YAMLRocksScalar`/`YAMLRocksSequence`/`YAMLRocksMapping`) saying how to render
that value, with an explicit tag and scalar style, or `None` to defer to the
built-in rendering.

Descriptors carry the original host objects, not pre-rendered nodes, so the
emitter drives the recursion and keeps owning indentation, flow, `sort_keys`,
and anchor detection. The path lowers to the round-trip node tree and emits
through the round-trip emitter (which already speaks per-node style, tags, and
flow) rather than the fast `Value` emitter, so plain `dumps` is untouched.

The represent path reproduces PyYAML's dump behavior so a host's representers
port verbatim: block sequences indent under their key, `OPT_SORT_KEYS` applies,
a standard tag the value already resolves to is elided while a custom tag is
kept and force-quoted, multi-line strings default to a `|` literal block, and a
shared object emits once with an anchor and aliases the repeats. Deferred values
render byte-for-byte like a plain `dumps`.

Adds the ADR (021), the descriptor types and stubs, a docs section, and tests.
@frenck frenck force-pushed the frenck/represent-protocol branch from 1eb0287 to 0c4b6f2 Compare July 9, 2026 18:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Comment thread src/ffi/represent.rs
Comment thread src/ffi/mod.rs
Comment thread src/roundtrip/emit.rs Outdated
Comment thread docs/src/content/docs/guides/dumping.md Outdated
Copilot AI review requested due to automatic review settings July 9, 2026 18:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Comment thread src/roundtrip/emit.rs Outdated
Comment thread docs/src/content/docs/guides/dumping.md Outdated
A value the `represent` callback defers on (`None`) previously went through a
narrow node builder that only handled basic builtins, so `default`/`serializers`
were ignored and a deferred datetime/dataclass raised where a plain `dumps`
succeeds. Route deferred non-container leaves through the same `python_to_value`
pipeline a plain `dumps` uses, then bridge the resulting `Value` to a node, so
`default`, `serializers`, and datetime/dataclass/numpy handling all compose and
deferred output stays byte-for-byte identical to a plain `dumps`.

Move `sort_keys` into the lowering (before values are lowered) so it orders keys
by the fast path's typed comparator (numbers numerically, not lexically),
matching plain `dumps`, and so anchor detection follows the final emission order
rather than risking an alias emitted before its anchor.
Copilot AI review requested due to automatic review settings July 10, 2026 04:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Comment thread src/ffi/mod.rs Outdated
Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs Outdated
Comment thread adr/021-emitter-represent-protocol-for-dumps.md Outdated
Comment thread src/ffi/represent.rs Outdated
Address the review of the `represent` deferred path. A value the callback
defers on used to go through a narrow node builder, so nested values inside a
set/dataclass/enum/numpy value or a `default` result never reached `represent`,
`default`/`serializers` were bypassed, and several emit options were dropped.

- Every value: a deferred compound decomposes into its child objects, which
  recurse back through `represent`; only a scalar leaf goes through the shared
  `python_to_value` pipeline, so `default`/`serializers` and datetime/dataclass/
  numpy handling compose and deferred output matches a plain `dumps`.
- Aliasing now matches PyYAML's `ignore_aliases` (everything except
  None/bool/int/float/str/bytes), so a shared set, dataclass, or custom object
  represented as a collection is deduped, and a cycle through one resolves to an
  alias instead of the depth error.
- `sort_keys` orders keys by the fast path's typed comparator (numbers
  numerically), derived directly from the key so `default`/`serializers` are not
  run twice, and with a total order so the sort cannot misbehave.
- Descriptor tags are validated with the emit-side tag rules.
- A block scalar inside a flow collection is downgraded to a quoted style
  (block scalars are invalid there).
- `OPT_FLOW_STYLE`, `OPT_EXPLICIT_START`, and `OPT_EXPLICIT_END` apply on the
  represent path.

Updates ADR-021, the docs, and the tests to match.
Copilot AI review requested due to automatic review settings July 10, 2026 05:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/ffi/convert/encode.rs (1)

525-554: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Eliminate duplication between numpy_child and numpy_to_value.

numpy_child (lines 531-554) and numpy_to_value (lines 564-588) share identical NUMPY_TYPES lookup and is_instance checks. If the numpy detection logic changes, it must be updated in two places. Refactor numpy_to_value to delegate to numpy_child:

♻️ Proposed refactor
 fn numpy_to_value(
     py: Python<'_>,
     obj: &Bound<'_, PyAny>,
     ctx: EncodeCtx<'_>,
 ) -> PyResult<Option<Value<'static>>> {
-    let types = NUMPY_TYPES.get_or_init(|| {
-        let numpy = py.import("numpy").ok()?;
-        let ndarray = numpy.getattr("ndarray").ok()?.unbind();
-        let generic = numpy.getattr("generic").ok()?.unbind();
-        Some((ndarray, generic))
-    });
-    let Some((ndarray, generic)) = types else {
-        return Ok(None);
-    };
-    if !(obj.is_instance(ndarray.bind(py))? || obj.is_instance(generic.bind(py))?) {
-        return Ok(None);
-    }
-    if let Ok(list) = obj.call_method0("tolist") {
-        return Ok(Some(python_to_value(py, &list, ctx)?));
-    }
-    if let Ok(item) = obj.call_method0("item") {
-        return Ok(Some(python_to_value(py, &item, ctx)?));
-    }
-    Ok(None)
+    if let Some(child) = numpy_child(py, obj)? {
+        return Ok(Some(python_to_value(py, &child, ctx)?));
+    }
+    Ok(None)
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9b53e5f5-e3e4-47f2-a25a-94f2abbd01c9

📥 Commits

Reviewing files that changed from the base of the PR and between b34024c and 5dbf617.

📒 Files selected for processing (8)
  • adr/021-emitter-represent-protocol-for-dumps.md
  • docs/src/content/docs/guides/dumping.md
  • src/ffi/convert/encode.rs
  • src/ffi/convert/mod.rs
  • src/ffi/mod.rs
  • src/ffi/represent.rs
  • src/roundtrip/emit.rs
  • tests/features/test_represent.py
✅ Files skipped from review due to trivial changes (1)
  • docs/src/content/docs/guides/dumping.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/ffi/mod.rs
  • src/roundtrip/emit.rs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs
Comment thread src/ffi/represent.rs
Comment thread src/ffi/represent.rs
Follow-up review fixes on the `represent` path:

- A transparent re-dispatch (an enum's value, a numpy value, or a `default`
  result) now goes through `lower` at the next depth, so it is depth-bounded and
  stack-guarded. A `default` that returns its argument resolves to a self-alias
  instead of recursing into a native stack overflow.
- Only an unrecognized-type error falls back to `default`; a genuine encode
  error (non-UTF-8 bytes, a lone surrogate) propagates unchanged rather than
  being masked by `default`, matching plain `dumps`.
- The round-trip emitter now writes the anchor/tag on a nested block-sequence
  item, so a shared list nested in a sequence emits `- &id001` before the alias
  instead of dropping the anchor (also fixes a latent round-trip gap).

Documents that an explicit scalar style is honored verbatim (the host picks a
representable style) and that a block style inside a flow collection is
downgraded to a quoted one.
Copilot AI review requested due to automatic review settings July 10, 2026 07:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Comment thread src/ffi/represent.rs
Comment thread src/ffi/represent.rs
Comment thread src/ffi/represent.rs
Comment thread src/ffi/represent.rs
Comment thread src/ffi/represent.rs
Further review fixes on the `represent` path:

- Never stamp an anchor onto an alias node (that produced the malformed
  `&id001 *id001`). A value that resolves only to itself (a `default` or
  serializer returning its own input) now raises cleanly instead.
- `tagged_node` lowers its inner through the depth-bounded `lower`, so a
  serializer that tags its own input aliases and is rejected rather than
  recursing into a native stack overflow.
- Canonical `tag:yaml.org,2002:*` tags are normalized to `!!*` before
  validation, so a callback ported from a PyYAML representer (which uses the
  canonical URI tags) passes validation and gets the standard-tag elision.
- Every object recorded for aliasing is retained for the whole lowering, so a
  freed temporary's reused address cannot be misread as an alias to an earlier
  value.
- Integer sort keys keep their exact value (i128), so two large i64 keys no
  longer collide as they would under f64.
An integer key beyond `i64` was compared with `extract::<f64>()`, which
raises `OverflowError` once the value exceeds `f64`'s range, dropping the
key to the `Other` rank (sorted after strings). Parse the decimal repr
instead, matching the fast path's `compare_keys` (which parses a `BigInt`
to `±inf`): the key stays numeric and two that saturate to the same infinity
tie on insertion order, so a sorted `represent` dump matches plain `dumps`.
Copilot AI review requested due to automatic review settings July 10, 2026 15:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs
Comment thread src/ffi/mod.rs Outdated
A deferred tagged null now emits as a bare `key: !x` / `- !x` in both
mapping and sequence position instead of falling through to `!x null`, which
reloads as the string "null" rather than an empty value (the mapping branch
also used to drop the tag entirely). This matches the fast path byte-for-byte
and is verified against the YAML test suite for round-trip fidelity.

A synthetic non-first block-mapping key carrying a tag or anchor now uses the
explicit `? key` form. An inline `!tag key:` after a previous entry has its
property read as the preceding value's node property, a reparse error when that
value was empty, so the `?` indicator opens a fresh key instead, matching the
fast emitter. Loaded keys are never synthetic, so their fidelity is untouched.

The synthetic AST is dismantled iteratively with `drop_node_tree` (GIL
detached) after emission, so a deeply nested represent tree cannot overflow the
native stack on its recursive drop, mirroring the loaded-document teardown.
Copilot AI review requested due to automatic review settings July 10, 2026 16:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Comment thread src/ffi/represent.rs
Comment thread src/ffi/represent.rs
The deferred `represent` path (a callback returning None for every value) is
contractually a plain `dumps`, but it emits through the round-trip emitter
rather than the fast `Value` encoder, so the two drifted on several shaping and
dispatch decisions. An exhaustive parity sweep (a corpus of every scalar type,
collection, datetime, dataclass, enum, and tagged value, in every structural
position, across the option flags) found six divergence classes; all are fixed:

- Block-scalar body indent in a sequence item now lands at `dash + 2` (was
  `dash + 4`) by making the round-trip emitter take an absolute body column
  instead of always adding a step, matching the fast encoder.
- A multi-line string mapping key is emitted inline double-quoted, and a
  collection key inline as a flow collection, rather than an explicit `?` block
  key (which the fast path has no equivalent of).
- `OPT_INDENT_2/4` and `OPT_INDENTLESS_SEQUENCES` are honored on the dump path.
- A datetime-like object (anything exposing `isoformat()`) is dispatched before
  Enum/dataclass, matching the fast path's `special_type_to_value` order.
- A non-primitive mapping key (datetime/UUID/Path) sorts under `OPT_SORT_KEYS`
  by the scalar the fast path renders, not dropped to input order.

Aliasing stays the one accepted divergence: `represent` keeps PyYAML-style
anchors for shared objects while plain `dumps` never aliases (ADR-021).

A parametrized parity test now sweeps the corpus and asserts deferred output
equals plain `dumps` byte-for-byte, so the two paths cannot silently drift
again. The deep-nesting represent test is bounded to a depth safely under the
interpreter's C recursion budget (the callback runs at every level).
Copilot AI review requested due to automatic review settings July 10, 2026 20:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread docs/src/content/docs/guides/dumping.md Outdated
Comment thread adr/021-emitter-represent-protocol-for-dumps.md Outdated
Comment thread src/roundtrip/emit.rs
Copilot AI review requested due to automatic review settings July 10, 2026 20:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs
Comment thread docs/src/content/docs/guides/dumping.md Outdated
Comment thread adr/021-emitter-represent-protocol-for-dumps.md Outdated
…tale indent docs

Sorting a mapping under OPT_SORT_KEYS converted each non-primitive key through
the pipeline with serializers still active, so a registered serializer ran once
during the sort and again when the key was lowered, and a real conversion error
was swallowed as an 'other' key. The sort conversion now disables both `default`
and `serializers`: a key only those could render is not a plain scalar and ranks
in input order (as `compare_keys` ranks the tagged node it lowers to), a
datetime/UUID/Path key still sorts by its rendered string, and a genuine error
(a non-UTF-8 bytes key) propagates instead of being swallowed.

Also correct the docs, ADR-021, and emitter comments: OPT_INDENT_4 and
OPT_INDENTLESS_SEQUENCES do apply on the represent path (they were wired into the
dump config in this PR); only line width remains unimplemented there.
Copilot AI review requested due to automatic review settings July 10, 2026 20:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs
…e, int subclass

- Dismantle the temporary Value built to rank a non-primitive sort key with
  drop_value_tree, so a deeply nested key cannot overflow the stack on its
  recursive drop.
- Rank a key the built-in conversion cannot render as Other (input order) rather
  than raising during the sort pass: represent runs first when the key is
  lowered and may rescue it (a non-UTF-8 bytes key, a custom object), and if it
  does not, lowering still raises. Refines the previous error-propagation, which
  raised before represent got its documented first look at the key.
- Reduce a beyond-i64 int-subclass key through a base int before parsing its f64
  sort rank, so an overridden __str__ cannot make it sort as NaN while plain
  dumps sorts by the real numeric value.
Copilot AI review requested due to automatic review settings July 10, 2026 21:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs Outdated
Comment thread src/ffi/represent.rs
…positions

An adversarial pass over the diff plus a widened parity sweep (now covering
tagged values) surfaced several byte-for-byte gaps for tagged values, all fixed
against the fast encoder:

- A tagged block collection at the document root indents its body one step under
  the tag (a tagged root sequence stays flush under OPT_INDENTLESS_SEQUENCES),
  instead of emitting the body flush against the tag line.
- A tagged empty null at the root keeps just its tag (`!t`) rather than
  `!t null`, which would reload as the string "null".
- A tagged block mapping as a sequence item indents by the configured step; a
  tagged nested sequence keeps the fixed two-column offset.
- A tagged sequence in mapping-value position always indents under its tag, even
  in indentless mode, so the tag binds to it on reload.

Also reject `width` together with `represent` explicitly (line folding is not
implemented on that path) rather than silently ignoring it, and record that in
the docs and ADR. The parity sweep now exercises tagged values across positions
and options; the two accepted edges (a shared tagged value cannot alias with its
tag; a tagged value as a mapping key renders as a valid explicit key) are
skipped with comments.
Copilot AI review requested due to automatic review settings July 12, 2026 09:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Comment thread src/ffi/represent.rs
// transparently to a child (enum value, numpy, a `default`/serializer
// result), returning the child's node, which already carries the child's
// anchor or is an alias.
let delegated = matches!(node.kind, YamlNodeKind::Alias(_)) || node.anchor.is_some();
Comment on lines +51 to +52
it. The fast emitter cannot emit a literal `|` block at all, has no per-node style
or per-node flow, and always shows a tag; adding all of that to the hot emitter is
…lock styles

- If lowering a later mapping entry or sequence item fails after an earlier
  deeply nested one was already built, dismantle the accumulated nodes
  iteratively (drop_node_pairs / drop_node_tree) before propagating the error,
  so their derived recursive Drop cannot overflow the native stack. Mirrors the
  success-path teardown.
- Reject an explicit literal/folded scalar style that cannot round-trip the
  value instead of silently emitting lossy YAML: folding rewrites interior line
  breaks, a literal auto-detects indentation from a leading-whitespace first
  line, and neither can hold a carriage return or non-printable control. A
  lossless literal (single or multi-line, no leading whitespace) still works, so
  the !lambda literal case is unaffected. A flow context still downgrades block
  styles to double-quoted as before.
Copilot AI review requested due to automatic review settings July 12, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-feature New features or options.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants