Skip to content

TOML encoder: prefer readable table sections over inline tables#2649

Merged
mikefarah merged 7 commits intomasterfrom
copilot/featurereadable-toml-output
Apr 12, 2026
Merged

TOML encoder: prefer readable table sections over inline tables#2649
mikefarah merged 7 commits intomasterfrom
copilot/featurereadable-toml-output

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 6, 2026

When encoding to TOML from YAML/JSON, nested mappings were collapsed into compact inline table syntax (key = { ... }) instead of idiomatic TOML table sections ([key]). This affected both direct YAML→TOML conversion and round-trips through YAML.

Changes

Encoder (encoder_toml.go)

  • encodeTopLevelEntry: Check EncodeHintInline/FlowStyle — only nodes marked as inline stay inline; all block mappings become table sections
  • encodeSeparateMapping: Count EncodeHintInline/FlowStyle mapping children as hasAttrs (they render as key = { ... }); use recursive encodeSeparateMapping in the "no attrs" loop
  • encodeMappingBodyWithPath: Emit non-inline child mappings as sub-table sections ([parent.child]) instead of inline tables

Decoder (decoder_toml.go)

  • Set EncodeHint: EncodeHintInline on decoded TOML inline tables, distinguishing them from block mappings so they survive re-encoding as inline
  • Set EncodeHint: EncodeHintSeparateBlock on decoded TOML table sections ([section]) and array tables ([[section]])

Core (candidate_node.go)

  • Replaced EncodeSeparate bool and TomlInline bool fields with a single EncodeHint EncodeHint enum field, eliminating the possibility of conflicting boolean combinations and making it easy to add further encoding options in future:
    • EncodeHintDefault (0) — encoder picks the representation (TOML block mappings default to [section])
    • EncodeHintSeparateBlock — force emission as a separate block/section header (TOML [section], HCL blocks)
    • EncodeHintInline — force emission as an inline/flow table (TOML inline tables)
  • UpdateAttributesFrom propagates EncodeHint so it survives DeeplyAssign deep-merges

Decoder/Encoder HCL (decoder_hcl.go, encoder_hcl.go)

  • Updated to use EncodeHint: EncodeHintSeparateBlock in place of the old EncodeSeparate: true

Tests (toml_test.go)

  • Add encode scenario type (YAML→TOML) and coverage for table section output
  • TOML roundtrip tests preserved: inline tables stay inline, table sections stay as sections

Before / After

# input YAML
arg:
  hello: foo
# before
arg = { hello = "foo" }

# after
[arg]
hello = "foo"

YAML flow mappings (arg: {hello: foo}) and TOML inline tables are preserved as TOML inline tables, respecting the author's explicit style intent. TOML table sections and YAML block mappings both produce readable [section] headers.

When converting from YAML/JSON to TOML, the encoder now always uses
readable TOML table section syntax ([section]) instead of compact inline
hash table syntax (key = { ... }), which better matches TOML's goal as
a human-focused configuration format.

Changes:
- decoder_toml.go: Mark inline TOML tables with FlowStyle so round-trips
  can be distinguished from YAML flow mappings
- encoder_toml.go:
  - encodeTopLevelEntry: use FlowStyle check instead of EncodeSeparate to
    decide inline vs table section (all block mappings now become tables)
  - encodeSeparateMapping: count FlowStyle children as attributes; use
    recursive encodeSeparateMapping for nested non-flow mappings
  - encodeMappingBodyWithPath: emit non-flow child mappings as sub-table
    sections instead of inline tables
- toml_test.go: add encode (YAML→TOML) test scenarios, update roundtrip
  expectations for inline tables (now expanded to table sections)

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/4824a219-6d5e-42e7-bca1-a8a277bf8c6a

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for readable TOML output in yq TOML encoder: prefer readable table sections over inline tables Apr 6, 2026
Copilot AI requested a review from mikefarah April 6, 2026 10:01
Comment thread pkg/yqlib/toml_test.go Outdated
input: rtInlineTableAttr,
expression: ".",
expected: rtInlineTableAttr,
expected: rtInlineTableAttrEncoded,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot the point of the roundtrip tests is to maintain the format as much as possible...this no longer does that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d1ffec1. The root cause was that FlowStyle (used in the previous commit to mark inline tables) also affects the YAML encoder, which was changing the TOML→YAML decode output.

The fix introduces a new TomlInline bool field on CandidateNode — TOML-specific and invisible to the YAML encoder. The TOML decoder sets it for inline tables, UpdateAttributesFrom propagates it through DeeplyAssign merges, and the TOML encoder checks it to decide inline vs table-section output.

TOML roundtrip tests are restored to their original expected values: inline tables stay inline, table sections stay as table sections.

…rve inline tables

FlowStyle affected YAML decode output (causing inline tables to appear as
YAML flow mappings). Replace it with a new TOML-specific TomlInline bool
on CandidateNode that:
- Is set by the TOML decoder for inline tables (not FlowStyle)
- Is copied by UpdateAttributesFrom so it survives DeeplyAssign merges
- Is checked by the TOML encoder alongside FlowStyle (for YAML flow maps)
- Has no effect on the YAML encoder, preserving existing TOML→YAML output

TOML roundtrip tests are restored to their original expected values (inline
tables stay inline, table sections stay as sections).

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/f59bdf62-6d16-4664-991b-38eb87c9d81c

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
Copilot AI requested a review from mikefarah April 8, 2026 07:08
Comment thread pkg/yqlib/candidate_node.go Outdated
EncodeSeparate bool
// For TOML: indicates that this mapping was originally a TOML inline table and should be
// re-encoded as an inline table rather than a separate table section.
TomlInline bool
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot how is TomlInline different to EncodeSeparate? They seem quite similar right? Perhaps EncodeSeparete just needs to default to true when decoding toml or something? Rather than adding another new property that's specific to TOML

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They're semantically opposite:

  • EncodeSeparate = true → "render as a [section] header"
  • TomlInline = true → "render as key = { ... } inline table"

Three cases need to be distinguished:

Source EncodeSeparate TomlInline Output
TOML table section true false [section]
TOML inline table false true key = { }
YAML block mapping false false [section] (encoder default)

Without TomlInline, cases 2 and 3 look identical (EncodeSeparate = false) so inline tables would expand to sections in roundtrip.

FlowStyle was the approach in the previous commit (b99f417), but it has two problems: (1) its propagation in UpdateAttributesFrom is conditional (if n.Style == 0), so it gets lost during DeeplyAssign's deep-merge — confirmed empirically, which is exactly why b99f417 broke roundtrip; (2) it changes TOML→YAML decode output (inline tables become flow YAML instead of block YAML).

TomlInline uses the same unconditional propagation pattern as EncodeSeparate, which is why it works reliably. Happy to explore any alternative approach if you have one in mind.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot

What about EncodeSeparate = true and TomlInline = true? That would be a conflict right?

I think booleans are going to get complex and conflict with each other. Maybe a better approach is to rethink EncodeSeparate, change it from a boolean to a int with bit flags EncodingOptions enumeration?

To being with, we could have 3 values:
Default (nil?): (encoder default) for TOML this would output "[section]"
SEPARATE_HEADER: explicitly output "[section]"
INLINE: explicitly render the table inline.

This lets us add more options later.

There's still a problem of specifying to conflicting bits though (e.g. SEPARATE_HEADER and INLINE). Not sure if there's something we can do that better..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 8c1cb6a. Replaced both EncodeSeparate bool and TomlInline bool with a single EncodeHint enum on CandidateNode:

type EncodeHint int

const (
    EncodeHintDefault      EncodeHint = iota // encoder default (TOML → [section])
    EncodeHintSeparateBlock                   // force separate block/section header
    EncodeHintInline                          // force inline / flow table
)
  • TOML [section] decoder → EncodeHintSeparateBlock
  • TOML inline table decoder → EncodeHintInline
  • HCL multi-block decoder → EncodeHintSeparateBlock
  • All other nodes → EncodeHintDefault (zero value, no change)

Adding more encoding options in the future is just a new constant; conflicting combinations are impossible since it's a single enum field.

Copilot AI and others added 2 commits April 11, 2026 02:41
@mikefarah
Copy link
Copy Markdown
Owner

@copilot fix the conflicts

…able-toml-output

# Conflicts:
#	pkg/yqlib/toml_test.go

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 12, 2026

@copilot fix the conflicts

Done in a297c15. The only conflict was in toml_test.go — both branches added new test cases at the same location. Resolved by keeping both: the encode-scenario tests from this PR and the special-character roundtrip tests from master. All tests pass.

@mikefarah
Copy link
Copy Markdown
Owner

@copilot Error: pkg/yqlib/encoder_toml.go:536:24: func (*tomlEncoder).hasStructuralChildren is unused (unused)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 12, 2026

@copilot Error: pkg/yqlib/encoder_toml.go:536:24: func (*tomlEncoder).hasStructuralChildren is unused (unused)

Fixed in 320b0de — removed the unused hasStructuralChildren method.

@mikefarah mikefarah marked this pull request as ready for review April 12, 2026 08:25
@mikefarah mikefarah merged commit 2927a28 into master Apr 12, 2026
5 checks passed
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.

yq TOML output prioritizes compact hash table syntax over readability

2 participants