Add an experimental opt-in libfyaml backend - #805

Merged
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10
Jul 2, 2026
Merged

Add an experimental opt-in libfyaml backend#805
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10

Conversation

@hsbt

@hsbthsbt commented Jul 1, 2026

Copy link
Copy Markdown
Member

Psych is tied to libyaml, which implements YAML 1.1 and rejects some valid YAML 1.2 documents. This adds an experimental alternative backend built on libfyaml, a fully YAML 1.2 compliant parser and emitter, compiled only when psych is built with --enable-libfyaml. Without the flag nothing changes and the default libyaml backend is used, and the flag is rejected on Windows where libfyaml is unsupported.

Because libfyaml follows YAML 1.2, the YAML 1.1 booleans yes, no, on, and off load as plain strings rather than true or false, which resolves the "Norway problem" where the country code no became false. This is keyed on Psych::BACKEND, so the libyaml default keeps its current behavior.

The parser and emitter are reimplemented against libfyaml's event API in translation units guarded by PSYCH_USE_LIBFYAML. Errors and positions are recovered from libfyaml's diagnostics, UTF-16 IO is transcoded to UTF-8, and scalar emission is matched to libyaml where it matters. The test suite is made backend-aware and gains positive coverage for the libfyaml behavior, and a new CI workflow builds with the flag on Linux and macOS and asserts the active backend before running the suite.

The backend is experimental. Its output is valid YAML but is formatted differently from libyaml in places and a few emitter edge cases are not yet matched, so libyaml remains the supported default.

hsbtand others added 2 commits July 1, 2026 06:48
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings July 1, 2026 02:04

CopilotAI 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

Adds an experimental, opt-in libfyaml backend to Psych to enable YAML 1.2–compliant parsing/emitting when built with --enable-libfyaml, while keeping the existing libyaml backend as the default. The change introduces new libfyaml-backed C implementations for the parser/emitter, exposes backend identification/version info, updates boolean/scalar scanning for YAML 1.2 semantics, and adjusts the test suite + CI to be backend-aware.

Changes:

  • Add PSYCH_USE_LIBFYAML build mode (--enable-libfyaml) with libfyaml-backed parser/emitter translation units and Psych::BACKEND / Psych.libfyaml_version.
  • Update scalar scanning and tests to reflect YAML 1.2 core schema behavior (notably yes/no/on/off as strings).
  • Add a dedicated GitHub Actions workflow to compile and run the suite with the libfyaml backend on Linux/macOS.

Reviewed changes

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

Show a summary per file
FileDescription
ext/psych/extconf.rbAdds --enable-libfyaml build option, pkg-config detection, and Windows rejection.
ext/psych/psych.hSwitches header include between yaml.h and libfyaml.h based on PSYCH_USE_LIBFYAML.
ext/psych/psych.cExposes Psych::BACKEND; adds Psych.libfyaml_version; adapts version reporting under libfyaml.
ext/psych/psych_parser.cCompiles out the libyaml parser when building with libfyaml.
ext/psych/psych_parser_fy.cNew libfyaml-backed parser implementation using libfyaml’s event API.
ext/psych/psych_emitter.cCompiles out the libyaml emitter when building with libfyaml.
ext/psych/psych_emitter_fy.cNew libfyaml-backed emitter implementation mapping Psych events to libfyaml emission.
lib/psych/scalar_scanner.rbMakes boolean scanning backend-aware (YAML 1.1 vs 1.2 boolean sets).
test/psych/helper.rbAdds libfyaml? helper for backend-aware tests.
test/psych/test_boolean.rbUpdates boolean tests to reflect YAML 1.2 behavior and adds Norway-problem coverage.
test/psych/test_psych.rbAdds backend/version assertions and skips unsupported/format-different cases under libfyaml.
test/psych/test_string.rbSkips YAML 1.1 boolean quoting assertions under libfyaml; adds libfyaml-specific expectations.
test/psych/visitors/test_to_ruby.rbSkips YAML 1.1 yes/no/on/off boolean visitor tests under libfyaml.
test/psych/test_yaml.rbSkips a YAML-spec fixture that depends on YAML 1.1 boolean typing under libfyaml.
test/psych/test_yaml_special_cases.rbSkips YAML 1.1 off boolean special-case under libfyaml.
test/psych/test_tree_builder.rbSkips location assertions that differ under libfyaml.
test/psych/test_symbol.rbSkips a known libfyaml emitter round-trip limitation for indicator-named symbols.
test/psych/test_set.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_parser.rbSkips mark position assertions that differ under libfyaml.
test/psych/test_omap.rbSkips tag-format assertion under libfyaml.
test/psych/test_json_tree.rbSkips JSON flow formatting assertions under libfyaml.
test/psych/test_exception.rbAdjusts exception context expectation (libfyaml diagnostics differ).
test/psych/test_encoding.rbSkips encoding-related expectations where libfyaml errors/tags differ.
test/psych/test_data.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_coder.rbSkips formatting/tag-synthesis expectations under libfyaml.
test/psych/json/test_stream.rbSkips JSON flow formatting assertions under libfyaml.
README.mdDocuments the experimental libfyaml backend, build flag, behavior differences, and backend detection.
.github/workflows/libfyaml.ymlAdds CI job that builds with --enable-libfyaml, verifies backend, and runs tests on Linux/macOS.

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

Comment threadext/psych/psych.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadtest/psych/test_string.rb
Comment threadext/psych/psych.c
Comment threadext/psych/psych_parser_fy.c Outdated
hsbtand others added 14 commits July 1, 2026 15:25
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a libfyaml? helper and guard the tests that intentionally diverge on
the experimental YAML 1.2 backend: yes/no/on/off are strings rather than
booleans, flow collections and block scalars are formatted differently,
tags and marks are reported differently, and non-ASCII tags/aliases are
rejected. test_boolean asserts the 1.2 string result directly; the
formatting and mark cases are skipped. The default libyaml build is
unaffected (every guard keys off Psych::BACKEND).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover what the libfyaml backend does distinctly, each placed with the
concern it belongs to: the Psych::BACKEND and Psych.libfyaml_version
checks in test_psych.rb, the YAML 1.2 "Norway problem" boolean case in
test_boolean.rb, and the "1.1 booleans are not quoted" emission case in
test_string.rb. The 1.2 assertions are skipped on the default libyaml
backend so the same suite passes under both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build psych with --enable-libfyaml and run the suite on Linux and macOS
(libfyaml is not supported on Windows). A verification step asserts that
Psych::BACKEND is actually libfyaml so the job cannot silently fall back
to libyaml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain how to enable it (--enable-libfyaml), that it is unsupported on
Windows, the YAML 1.2 boolean behavior it brings (the "Norway problem"
fix), and how to check the active backend, with an experimental caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify command was a plain YAML scalar, so the " #{Psych::BACKEND}"
started a YAML comment and truncated the shell command, leaving an
unterminated quote. Use a block scalar so the interpolation survives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An adversarial review confirmed two backend-visible differences that were
not written down. Scalars emitted with the default style can be formatted
differently from libyaml, and Psych::SyntaxError#context is always nil
because libfyaml keeps the whole diagnostic in #problem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the rough parse/emit benchmark so users pick the backend on purpose:
libfyaml is for YAML 1.2 semantics, and libyaml stays the choice when emit
throughput matters since libfyaml dumps about 1.7x to 1.9x slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hsbt
hsbtforce-pushed the claude/cool-carson-18ae10 branch from 68098bf to d185ff2CompareJuly 1, 2026 06:35

@tenderlovetenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. I think we should eventually remove the omit calls, but I think this PR is great. Thank you for doing this!!

@hsbt

hsbt commented Jul 2, 2026

Copy link
Copy Markdown
MemberAuthor

@tenderlove Thanks for your looking. I forgot to mention performance resulst in this pull request. I copy-and-paste from README.md update.

This backend targets YAML 1.2 compliance, not speed. In a rough
single-machine benchmark that loads and dumps in-memory documents, parsing
was roughly on par with libyaml (sometimes faster on string-heavy input),
while emitting was about 1.7x to 1.9x slower.

I'm not familiar with C code. But I did buttle-reviewed by three Claude subagents with API/Security/UX forcuses and confirmed them.

@hsbt
hsbt merged commit 7d5e036 into masterJul 2, 2026
194 checks passed
@hsbt
hsbt deleted the claude/cool-carson-18ae10 branch July 2, 2026 02:39
peterzhu2118 added a commit to ruby/ruby that referenced this pull request Jul 2, 2026
ruby/psych#805 added files psych_emitter_fy.c and
psych_parser_fy.c so we need to update the depend file too.
hsbt added a commit to ruby/ruby that referenced this pull request Aug 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@hsbt@tenderlove
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add an experimental opt-in libfyaml backend - #805

Merged
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10
Jul 2, 2026
Merged

Add an experimental opt-in libfyaml backend#805
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10

Conversation

@hsbt

@hsbthsbt commented Jul 1, 2026

Copy link
Copy Markdown
Member

Psych is tied to libyaml, which implements YAML 1.1 and rejects some valid YAML 1.2 documents. This adds an experimental alternative backend built on libfyaml, a fully YAML 1.2 compliant parser and emitter, compiled only when psych is built with --enable-libfyaml. Without the flag nothing changes and the default libyaml backend is used, and the flag is rejected on Windows where libfyaml is unsupported.

Because libfyaml follows YAML 1.2, the YAML 1.1 booleans yes, no, on, and off load as plain strings rather than true or false, which resolves the "Norway problem" where the country code no became false. This is keyed on Psych::BACKEND, so the libyaml default keeps its current behavior.

The parser and emitter are reimplemented against libfyaml's event API in translation units guarded by PSYCH_USE_LIBFYAML. Errors and positions are recovered from libfyaml's diagnostics, UTF-16 IO is transcoded to UTF-8, and scalar emission is matched to libyaml where it matters. The test suite is made backend-aware and gains positive coverage for the libfyaml behavior, and a new CI workflow builds with the flag on Linux and macOS and asserts the active backend before running the suite.

The backend is experimental. Its output is valid YAML but is formatted differently from libyaml in places and a few emitter edge cases are not yet matched, so libyaml remains the supported default.

hsbtand others added 2 commits July 1, 2026 06:48
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings July 1, 2026 02:04

CopilotAI 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

Adds an experimental, opt-in libfyaml backend to Psych to enable YAML 1.2–compliant parsing/emitting when built with --enable-libfyaml, while keeping the existing libyaml backend as the default. The change introduces new libfyaml-backed C implementations for the parser/emitter, exposes backend identification/version info, updates boolean/scalar scanning for YAML 1.2 semantics, and adjusts the test suite + CI to be backend-aware.

Changes:

  • Add PSYCH_USE_LIBFYAML build mode (--enable-libfyaml) with libfyaml-backed parser/emitter translation units and Psych::BACKEND / Psych.libfyaml_version.
  • Update scalar scanning and tests to reflect YAML 1.2 core schema behavior (notably yes/no/on/off as strings).
  • Add a dedicated GitHub Actions workflow to compile and run the suite with the libfyaml backend on Linux/macOS.

Reviewed changes

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

Show a summary per file
FileDescription
ext/psych/extconf.rbAdds --enable-libfyaml build option, pkg-config detection, and Windows rejection.
ext/psych/psych.hSwitches header include between yaml.h and libfyaml.h based on PSYCH_USE_LIBFYAML.
ext/psych/psych.cExposes Psych::BACKEND; adds Psych.libfyaml_version; adapts version reporting under libfyaml.
ext/psych/psych_parser.cCompiles out the libyaml parser when building with libfyaml.
ext/psych/psych_parser_fy.cNew libfyaml-backed parser implementation using libfyaml’s event API.
ext/psych/psych_emitter.cCompiles out the libyaml emitter when building with libfyaml.
ext/psych/psych_emitter_fy.cNew libfyaml-backed emitter implementation mapping Psych events to libfyaml emission.
lib/psych/scalar_scanner.rbMakes boolean scanning backend-aware (YAML 1.1 vs 1.2 boolean sets).
test/psych/helper.rbAdds libfyaml? helper for backend-aware tests.
test/psych/test_boolean.rbUpdates boolean tests to reflect YAML 1.2 behavior and adds Norway-problem coverage.
test/psych/test_psych.rbAdds backend/version assertions and skips unsupported/format-different cases under libfyaml.
test/psych/test_string.rbSkips YAML 1.1 boolean quoting assertions under libfyaml; adds libfyaml-specific expectations.
test/psych/visitors/test_to_ruby.rbSkips YAML 1.1 yes/no/on/off boolean visitor tests under libfyaml.
test/psych/test_yaml.rbSkips a YAML-spec fixture that depends on YAML 1.1 boolean typing under libfyaml.
test/psych/test_yaml_special_cases.rbSkips YAML 1.1 off boolean special-case under libfyaml.
test/psych/test_tree_builder.rbSkips location assertions that differ under libfyaml.
test/psych/test_symbol.rbSkips a known libfyaml emitter round-trip limitation for indicator-named symbols.
test/psych/test_set.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_parser.rbSkips mark position assertions that differ under libfyaml.
test/psych/test_omap.rbSkips tag-format assertion under libfyaml.
test/psych/test_json_tree.rbSkips JSON flow formatting assertions under libfyaml.
test/psych/test_exception.rbAdjusts exception context expectation (libfyaml diagnostics differ).
test/psych/test_encoding.rbSkips encoding-related expectations where libfyaml errors/tags differ.
test/psych/test_data.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_coder.rbSkips formatting/tag-synthesis expectations under libfyaml.
test/psych/json/test_stream.rbSkips JSON flow formatting assertions under libfyaml.
README.mdDocuments the experimental libfyaml backend, build flag, behavior differences, and backend detection.
.github/workflows/libfyaml.ymlAdds CI job that builds with --enable-libfyaml, verifies backend, and runs tests on Linux/macOS.

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

Comment threadext/psych/psych.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadtest/psych/test_string.rb
Comment threadext/psych/psych.c
Comment threadext/psych/psych_parser_fy.c Outdated
hsbtand others added 14 commits July 1, 2026 15:25
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a libfyaml? helper and guard the tests that intentionally diverge on
the experimental YAML 1.2 backend: yes/no/on/off are strings rather than
booleans, flow collections and block scalars are formatted differently,
tags and marks are reported differently, and non-ASCII tags/aliases are
rejected. test_boolean asserts the 1.2 string result directly; the
formatting and mark cases are skipped. The default libyaml build is
unaffected (every guard keys off Psych::BACKEND).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover what the libfyaml backend does distinctly, each placed with the
concern it belongs to: the Psych::BACKEND and Psych.libfyaml_version
checks in test_psych.rb, the YAML 1.2 "Norway problem" boolean case in
test_boolean.rb, and the "1.1 booleans are not quoted" emission case in
test_string.rb. The 1.2 assertions are skipped on the default libyaml
backend so the same suite passes under both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build psych with --enable-libfyaml and run the suite on Linux and macOS
(libfyaml is not supported on Windows). A verification step asserts that
Psych::BACKEND is actually libfyaml so the job cannot silently fall back
to libyaml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain how to enable it (--enable-libfyaml), that it is unsupported on
Windows, the YAML 1.2 boolean behavior it brings (the "Norway problem"
fix), and how to check the active backend, with an experimental caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify command was a plain YAML scalar, so the " #{Psych::BACKEND}"
started a YAML comment and truncated the shell command, leaving an
unterminated quote. Use a block scalar so the interpolation survives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An adversarial review confirmed two backend-visible differences that were
not written down. Scalars emitted with the default style can be formatted
differently from libyaml, and Psych::SyntaxError#context is always nil
because libfyaml keeps the whole diagnostic in #problem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the rough parse/emit benchmark so users pick the backend on purpose:
libfyaml is for YAML 1.2 semantics, and libyaml stays the choice when emit
throughput matters since libfyaml dumps about 1.7x to 1.9x slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hsbt
hsbtforce-pushed the claude/cool-carson-18ae10 branch from 68098bf to d185ff2CompareJuly 1, 2026 06:35

@tenderlovetenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. I think we should eventually remove the omit calls, but I think this PR is great. Thank you for doing this!!

@hsbt

hsbt commented Jul 2, 2026

Copy link
Copy Markdown
MemberAuthor

@tenderlove Thanks for your looking. I forgot to mention performance resulst in this pull request. I copy-and-paste from README.md update.

This backend targets YAML 1.2 compliance, not speed. In a rough
single-machine benchmark that loads and dumps in-memory documents, parsing
was roughly on par with libyaml (sometimes faster on string-heavy input),
while emitting was about 1.7x to 1.9x slower.

I'm not familiar with C code. But I did buttle-reviewed by three Claude subagents with API/Security/UX forcuses and confirmed them.

@hsbt
hsbt merged commit 7d5e036 into masterJul 2, 2026
194 checks passed
@hsbt
hsbt deleted the claude/cool-carson-18ae10 branch July 2, 2026 02:39
peterzhu2118 added a commit to ruby/ruby that referenced this pull request Jul 2, 2026
ruby/psych#805 added files psych_emitter_fy.c and
psych_parser_fy.c so we need to update the depend file too.
hsbt added a commit to ruby/ruby that referenced this pull request Aug 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@hsbt@tenderlove
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add an experimental opt-in libfyaml backend - #805

Merged
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10
Jul 2, 2026
Merged

Add an experimental opt-in libfyaml backend#805
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10

Conversation

@hsbt

@hsbthsbt commented Jul 1, 2026

Copy link
Copy Markdown
Member

Psych is tied to libyaml, which implements YAML 1.1 and rejects some valid YAML 1.2 documents. This adds an experimental alternative backend built on libfyaml, a fully YAML 1.2 compliant parser and emitter, compiled only when psych is built with --enable-libfyaml. Without the flag nothing changes and the default libyaml backend is used, and the flag is rejected on Windows where libfyaml is unsupported.

Because libfyaml follows YAML 1.2, the YAML 1.1 booleans yes, no, on, and off load as plain strings rather than true or false, which resolves the "Norway problem" where the country code no became false. This is keyed on Psych::BACKEND, so the libyaml default keeps its current behavior.

The parser and emitter are reimplemented against libfyaml's event API in translation units guarded by PSYCH_USE_LIBFYAML. Errors and positions are recovered from libfyaml's diagnostics, UTF-16 IO is transcoded to UTF-8, and scalar emission is matched to libyaml where it matters. The test suite is made backend-aware and gains positive coverage for the libfyaml behavior, and a new CI workflow builds with the flag on Linux and macOS and asserts the active backend before running the suite.

The backend is experimental. Its output is valid YAML but is formatted differently from libyaml in places and a few emitter edge cases are not yet matched, so libyaml remains the supported default.

hsbtand others added 2 commits July 1, 2026 06:48
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings July 1, 2026 02:04

CopilotAI 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

Adds an experimental, opt-in libfyaml backend to Psych to enable YAML 1.2–compliant parsing/emitting when built with --enable-libfyaml, while keeping the existing libyaml backend as the default. The change introduces new libfyaml-backed C implementations for the parser/emitter, exposes backend identification/version info, updates boolean/scalar scanning for YAML 1.2 semantics, and adjusts the test suite + CI to be backend-aware.

Changes:

  • Add PSYCH_USE_LIBFYAML build mode (--enable-libfyaml) with libfyaml-backed parser/emitter translation units and Psych::BACKEND / Psych.libfyaml_version.
  • Update scalar scanning and tests to reflect YAML 1.2 core schema behavior (notably yes/no/on/off as strings).
  • Add a dedicated GitHub Actions workflow to compile and run the suite with the libfyaml backend on Linux/macOS.

Reviewed changes

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

Show a summary per file
FileDescription
ext/psych/extconf.rbAdds --enable-libfyaml build option, pkg-config detection, and Windows rejection.
ext/psych/psych.hSwitches header include between yaml.h and libfyaml.h based on PSYCH_USE_LIBFYAML.
ext/psych/psych.cExposes Psych::BACKEND; adds Psych.libfyaml_version; adapts version reporting under libfyaml.
ext/psych/psych_parser.cCompiles out the libyaml parser when building with libfyaml.
ext/psych/psych_parser_fy.cNew libfyaml-backed parser implementation using libfyaml’s event API.
ext/psych/psych_emitter.cCompiles out the libyaml emitter when building with libfyaml.
ext/psych/psych_emitter_fy.cNew libfyaml-backed emitter implementation mapping Psych events to libfyaml emission.
lib/psych/scalar_scanner.rbMakes boolean scanning backend-aware (YAML 1.1 vs 1.2 boolean sets).
test/psych/helper.rbAdds libfyaml? helper for backend-aware tests.
test/psych/test_boolean.rbUpdates boolean tests to reflect YAML 1.2 behavior and adds Norway-problem coverage.
test/psych/test_psych.rbAdds backend/version assertions and skips unsupported/format-different cases under libfyaml.
test/psych/test_string.rbSkips YAML 1.1 boolean quoting assertions under libfyaml; adds libfyaml-specific expectations.
test/psych/visitors/test_to_ruby.rbSkips YAML 1.1 yes/no/on/off boolean visitor tests under libfyaml.
test/psych/test_yaml.rbSkips a YAML-spec fixture that depends on YAML 1.1 boolean typing under libfyaml.
test/psych/test_yaml_special_cases.rbSkips YAML 1.1 off boolean special-case under libfyaml.
test/psych/test_tree_builder.rbSkips location assertions that differ under libfyaml.
test/psych/test_symbol.rbSkips a known libfyaml emitter round-trip limitation for indicator-named symbols.
test/psych/test_set.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_parser.rbSkips mark position assertions that differ under libfyaml.
test/psych/test_omap.rbSkips tag-format assertion under libfyaml.
test/psych/test_json_tree.rbSkips JSON flow formatting assertions under libfyaml.
test/psych/test_exception.rbAdjusts exception context expectation (libfyaml diagnostics differ).
test/psych/test_encoding.rbSkips encoding-related expectations where libfyaml errors/tags differ.
test/psych/test_data.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_coder.rbSkips formatting/tag-synthesis expectations under libfyaml.
test/psych/json/test_stream.rbSkips JSON flow formatting assertions under libfyaml.
README.mdDocuments the experimental libfyaml backend, build flag, behavior differences, and backend detection.
.github/workflows/libfyaml.ymlAdds CI job that builds with --enable-libfyaml, verifies backend, and runs tests on Linux/macOS.

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

Comment threadext/psych/psych.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadtest/psych/test_string.rb
Comment threadext/psych/psych.c
Comment threadext/psych/psych_parser_fy.c Outdated
hsbtand others added 14 commits July 1, 2026 15:25
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a libfyaml? helper and guard the tests that intentionally diverge on
the experimental YAML 1.2 backend: yes/no/on/off are strings rather than
booleans, flow collections and block scalars are formatted differently,
tags and marks are reported differently, and non-ASCII tags/aliases are
rejected. test_boolean asserts the 1.2 string result directly; the
formatting and mark cases are skipped. The default libyaml build is
unaffected (every guard keys off Psych::BACKEND).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover what the libfyaml backend does distinctly, each placed with the
concern it belongs to: the Psych::BACKEND and Psych.libfyaml_version
checks in test_psych.rb, the YAML 1.2 "Norway problem" boolean case in
test_boolean.rb, and the "1.1 booleans are not quoted" emission case in
test_string.rb. The 1.2 assertions are skipped on the default libyaml
backend so the same suite passes under both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build psych with --enable-libfyaml and run the suite on Linux and macOS
(libfyaml is not supported on Windows). A verification step asserts that
Psych::BACKEND is actually libfyaml so the job cannot silently fall back
to libyaml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain how to enable it (--enable-libfyaml), that it is unsupported on
Windows, the YAML 1.2 boolean behavior it brings (the "Norway problem"
fix), and how to check the active backend, with an experimental caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify command was a plain YAML scalar, so the " #{Psych::BACKEND}"
started a YAML comment and truncated the shell command, leaving an
unterminated quote. Use a block scalar so the interpolation survives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An adversarial review confirmed two backend-visible differences that were
not written down. Scalars emitted with the default style can be formatted
differently from libyaml, and Psych::SyntaxError#context is always nil
because libfyaml keeps the whole diagnostic in #problem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the rough parse/emit benchmark so users pick the backend on purpose:
libfyaml is for YAML 1.2 semantics, and libyaml stays the choice when emit
throughput matters since libfyaml dumps about 1.7x to 1.9x slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hsbt
hsbtforce-pushed the claude/cool-carson-18ae10 branch from 68098bf to d185ff2CompareJuly 1, 2026 06:35

@tenderlovetenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. I think we should eventually remove the omit calls, but I think this PR is great. Thank you for doing this!!

@hsbt

hsbt commented Jul 2, 2026

Copy link
Copy Markdown
MemberAuthor

@tenderlove Thanks for your looking. I forgot to mention performance resulst in this pull request. I copy-and-paste from README.md update.

This backend targets YAML 1.2 compliance, not speed. In a rough
single-machine benchmark that loads and dumps in-memory documents, parsing
was roughly on par with libyaml (sometimes faster on string-heavy input),
while emitting was about 1.7x to 1.9x slower.

I'm not familiar with C code. But I did buttle-reviewed by three Claude subagents with API/Security/UX forcuses and confirmed them.

@hsbt
hsbt merged commit 7d5e036 into masterJul 2, 2026
194 checks passed
@hsbt
hsbt deleted the claude/cool-carson-18ae10 branch July 2, 2026 02:39
peterzhu2118 added a commit to ruby/ruby that referenced this pull request Jul 2, 2026
ruby/psych#805 added files psych_emitter_fy.c and
psych_parser_fy.c so we need to update the depend file too.
hsbt added a commit to ruby/ruby that referenced this pull request Aug 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@hsbt@tenderlove
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add an experimental opt-in libfyaml backend - #805

Merged
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10
Jul 2, 2026
Merged

Add an experimental opt-in libfyaml backend#805
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10

Conversation

@hsbt

@hsbthsbt commented Jul 1, 2026

Copy link
Copy Markdown
Member

Psych is tied to libyaml, which implements YAML 1.1 and rejects some valid YAML 1.2 documents. This adds an experimental alternative backend built on libfyaml, a fully YAML 1.2 compliant parser and emitter, compiled only when psych is built with --enable-libfyaml. Without the flag nothing changes and the default libyaml backend is used, and the flag is rejected on Windows where libfyaml is unsupported.

Because libfyaml follows YAML 1.2, the YAML 1.1 booleans yes, no, on, and off load as plain strings rather than true or false, which resolves the "Norway problem" where the country code no became false. This is keyed on Psych::BACKEND, so the libyaml default keeps its current behavior.

The parser and emitter are reimplemented against libfyaml's event API in translation units guarded by PSYCH_USE_LIBFYAML. Errors and positions are recovered from libfyaml's diagnostics, UTF-16 IO is transcoded to UTF-8, and scalar emission is matched to libyaml where it matters. The test suite is made backend-aware and gains positive coverage for the libfyaml behavior, and a new CI workflow builds with the flag on Linux and macOS and asserts the active backend before running the suite.

The backend is experimental. Its output is valid YAML but is formatted differently from libyaml in places and a few emitter edge cases are not yet matched, so libyaml remains the supported default.

hsbtand others added 2 commits July 1, 2026 06:48
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings July 1, 2026 02:04

CopilotAI 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

Adds an experimental, opt-in libfyaml backend to Psych to enable YAML 1.2–compliant parsing/emitting when built with --enable-libfyaml, while keeping the existing libyaml backend as the default. The change introduces new libfyaml-backed C implementations for the parser/emitter, exposes backend identification/version info, updates boolean/scalar scanning for YAML 1.2 semantics, and adjusts the test suite + CI to be backend-aware.

Changes:

  • Add PSYCH_USE_LIBFYAML build mode (--enable-libfyaml) with libfyaml-backed parser/emitter translation units and Psych::BACKEND / Psych.libfyaml_version.
  • Update scalar scanning and tests to reflect YAML 1.2 core schema behavior (notably yes/no/on/off as strings).
  • Add a dedicated GitHub Actions workflow to compile and run the suite with the libfyaml backend on Linux/macOS.

Reviewed changes

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

Show a summary per file
FileDescription
ext/psych/extconf.rbAdds --enable-libfyaml build option, pkg-config detection, and Windows rejection.
ext/psych/psych.hSwitches header include between yaml.h and libfyaml.h based on PSYCH_USE_LIBFYAML.
ext/psych/psych.cExposes Psych::BACKEND; adds Psych.libfyaml_version; adapts version reporting under libfyaml.
ext/psych/psych_parser.cCompiles out the libyaml parser when building with libfyaml.
ext/psych/psych_parser_fy.cNew libfyaml-backed parser implementation using libfyaml’s event API.
ext/psych/psych_emitter.cCompiles out the libyaml emitter when building with libfyaml.
ext/psych/psych_emitter_fy.cNew libfyaml-backed emitter implementation mapping Psych events to libfyaml emission.
lib/psych/scalar_scanner.rbMakes boolean scanning backend-aware (YAML 1.1 vs 1.2 boolean sets).
test/psych/helper.rbAdds libfyaml? helper for backend-aware tests.
test/psych/test_boolean.rbUpdates boolean tests to reflect YAML 1.2 behavior and adds Norway-problem coverage.
test/psych/test_psych.rbAdds backend/version assertions and skips unsupported/format-different cases under libfyaml.
test/psych/test_string.rbSkips YAML 1.1 boolean quoting assertions under libfyaml; adds libfyaml-specific expectations.
test/psych/visitors/test_to_ruby.rbSkips YAML 1.1 yes/no/on/off boolean visitor tests under libfyaml.
test/psych/test_yaml.rbSkips a YAML-spec fixture that depends on YAML 1.1 boolean typing under libfyaml.
test/psych/test_yaml_special_cases.rbSkips YAML 1.1 off boolean special-case under libfyaml.
test/psych/test_tree_builder.rbSkips location assertions that differ under libfyaml.
test/psych/test_symbol.rbSkips a known libfyaml emitter round-trip limitation for indicator-named symbols.
test/psych/test_set.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_parser.rbSkips mark position assertions that differ under libfyaml.
test/psych/test_omap.rbSkips tag-format assertion under libfyaml.
test/psych/test_json_tree.rbSkips JSON flow formatting assertions under libfyaml.
test/psych/test_exception.rbAdjusts exception context expectation (libfyaml diagnostics differ).
test/psych/test_encoding.rbSkips encoding-related expectations where libfyaml errors/tags differ.
test/psych/test_data.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_coder.rbSkips formatting/tag-synthesis expectations under libfyaml.
test/psych/json/test_stream.rbSkips JSON flow formatting assertions under libfyaml.
README.mdDocuments the experimental libfyaml backend, build flag, behavior differences, and backend detection.
.github/workflows/libfyaml.ymlAdds CI job that builds with --enable-libfyaml, verifies backend, and runs tests on Linux/macOS.

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

Comment threadext/psych/psych.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadtest/psych/test_string.rb
Comment threadext/psych/psych.c
Comment threadext/psych/psych_parser_fy.c Outdated
hsbtand others added 14 commits July 1, 2026 15:25
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a libfyaml? helper and guard the tests that intentionally diverge on
the experimental YAML 1.2 backend: yes/no/on/off are strings rather than
booleans, flow collections and block scalars are formatted differently,
tags and marks are reported differently, and non-ASCII tags/aliases are
rejected. test_boolean asserts the 1.2 string result directly; the
formatting and mark cases are skipped. The default libyaml build is
unaffected (every guard keys off Psych::BACKEND).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover what the libfyaml backend does distinctly, each placed with the
concern it belongs to: the Psych::BACKEND and Psych.libfyaml_version
checks in test_psych.rb, the YAML 1.2 "Norway problem" boolean case in
test_boolean.rb, and the "1.1 booleans are not quoted" emission case in
test_string.rb. The 1.2 assertions are skipped on the default libyaml
backend so the same suite passes under both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build psych with --enable-libfyaml and run the suite on Linux and macOS
(libfyaml is not supported on Windows). A verification step asserts that
Psych::BACKEND is actually libfyaml so the job cannot silently fall back
to libyaml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain how to enable it (--enable-libfyaml), that it is unsupported on
Windows, the YAML 1.2 boolean behavior it brings (the "Norway problem"
fix), and how to check the active backend, with an experimental caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify command was a plain YAML scalar, so the " #{Psych::BACKEND}"
started a YAML comment and truncated the shell command, leaving an
unterminated quote. Use a block scalar so the interpolation survives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An adversarial review confirmed two backend-visible differences that were
not written down. Scalars emitted with the default style can be formatted
differently from libyaml, and Psych::SyntaxError#context is always nil
because libfyaml keeps the whole diagnostic in #problem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the rough parse/emit benchmark so users pick the backend on purpose:
libfyaml is for YAML 1.2 semantics, and libyaml stays the choice when emit
throughput matters since libfyaml dumps about 1.7x to 1.9x slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hsbt
hsbtforce-pushed the claude/cool-carson-18ae10 branch from 68098bf to d185ff2CompareJuly 1, 2026 06:35

@tenderlovetenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. I think we should eventually remove the omit calls, but I think this PR is great. Thank you for doing this!!

@hsbt

hsbt commented Jul 2, 2026

Copy link
Copy Markdown
MemberAuthor

@tenderlove Thanks for your looking. I forgot to mention performance resulst in this pull request. I copy-and-paste from README.md update.

This backend targets YAML 1.2 compliance, not speed. In a rough
single-machine benchmark that loads and dumps in-memory documents, parsing
was roughly on par with libyaml (sometimes faster on string-heavy input),
while emitting was about 1.7x to 1.9x slower.

I'm not familiar with C code. But I did buttle-reviewed by three Claude subagents with API/Security/UX forcuses and confirmed them.

@hsbt
hsbt merged commit 7d5e036 into masterJul 2, 2026
194 checks passed
@hsbt
hsbt deleted the claude/cool-carson-18ae10 branch July 2, 2026 02:39
peterzhu2118 added a commit to ruby/ruby that referenced this pull request Jul 2, 2026
ruby/psych#805 added files psych_emitter_fy.c and
psych_parser_fy.c so we need to update the depend file too.
hsbt added a commit to ruby/ruby that referenced this pull request Aug 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@hsbt@tenderlove
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add an experimental opt-in libfyaml backend - #805

Merged
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10
Jul 2, 2026
Merged

Add an experimental opt-in libfyaml backend#805
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10

Conversation

@hsbt

@hsbthsbt commented Jul 1, 2026

Copy link
Copy Markdown
Member

Psych is tied to libyaml, which implements YAML 1.1 and rejects some valid YAML 1.2 documents. This adds an experimental alternative backend built on libfyaml, a fully YAML 1.2 compliant parser and emitter, compiled only when psych is built with --enable-libfyaml. Without the flag nothing changes and the default libyaml backend is used, and the flag is rejected on Windows where libfyaml is unsupported.

Because libfyaml follows YAML 1.2, the YAML 1.1 booleans yes, no, on, and off load as plain strings rather than true or false, which resolves the "Norway problem" where the country code no became false. This is keyed on Psych::BACKEND, so the libyaml default keeps its current behavior.

The parser and emitter are reimplemented against libfyaml's event API in translation units guarded by PSYCH_USE_LIBFYAML. Errors and positions are recovered from libfyaml's diagnostics, UTF-16 IO is transcoded to UTF-8, and scalar emission is matched to libyaml where it matters. The test suite is made backend-aware and gains positive coverage for the libfyaml behavior, and a new CI workflow builds with the flag on Linux and macOS and asserts the active backend before running the suite.

The backend is experimental. Its output is valid YAML but is formatted differently from libyaml in places and a few emitter edge cases are not yet matched, so libyaml remains the supported default.

hsbtand others added 2 commits July 1, 2026 06:48
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings July 1, 2026 02:04

CopilotAI 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

Adds an experimental, opt-in libfyaml backend to Psych to enable YAML 1.2–compliant parsing/emitting when built with --enable-libfyaml, while keeping the existing libyaml backend as the default. The change introduces new libfyaml-backed C implementations for the parser/emitter, exposes backend identification/version info, updates boolean/scalar scanning for YAML 1.2 semantics, and adjusts the test suite + CI to be backend-aware.

Changes:

  • Add PSYCH_USE_LIBFYAML build mode (--enable-libfyaml) with libfyaml-backed parser/emitter translation units and Psych::BACKEND / Psych.libfyaml_version.
  • Update scalar scanning and tests to reflect YAML 1.2 core schema behavior (notably yes/no/on/off as strings).
  • Add a dedicated GitHub Actions workflow to compile and run the suite with the libfyaml backend on Linux/macOS.

Reviewed changes

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

Show a summary per file
FileDescription
ext/psych/extconf.rbAdds --enable-libfyaml build option, pkg-config detection, and Windows rejection.
ext/psych/psych.hSwitches header include between yaml.h and libfyaml.h based on PSYCH_USE_LIBFYAML.
ext/psych/psych.cExposes Psych::BACKEND; adds Psych.libfyaml_version; adapts version reporting under libfyaml.
ext/psych/psych_parser.cCompiles out the libyaml parser when building with libfyaml.
ext/psych/psych_parser_fy.cNew libfyaml-backed parser implementation using libfyaml’s event API.
ext/psych/psych_emitter.cCompiles out the libyaml emitter when building with libfyaml.
ext/psych/psych_emitter_fy.cNew libfyaml-backed emitter implementation mapping Psych events to libfyaml emission.
lib/psych/scalar_scanner.rbMakes boolean scanning backend-aware (YAML 1.1 vs 1.2 boolean sets).
test/psych/helper.rbAdds libfyaml? helper for backend-aware tests.
test/psych/test_boolean.rbUpdates boolean tests to reflect YAML 1.2 behavior and adds Norway-problem coverage.
test/psych/test_psych.rbAdds backend/version assertions and skips unsupported/format-different cases under libfyaml.
test/psych/test_string.rbSkips YAML 1.1 boolean quoting assertions under libfyaml; adds libfyaml-specific expectations.
test/psych/visitors/test_to_ruby.rbSkips YAML 1.1 yes/no/on/off boolean visitor tests under libfyaml.
test/psych/test_yaml.rbSkips a YAML-spec fixture that depends on YAML 1.1 boolean typing under libfyaml.
test/psych/test_yaml_special_cases.rbSkips YAML 1.1 off boolean special-case under libfyaml.
test/psych/test_tree_builder.rbSkips location assertions that differ under libfyaml.
test/psych/test_symbol.rbSkips a known libfyaml emitter round-trip limitation for indicator-named symbols.
test/psych/test_set.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_parser.rbSkips mark position assertions that differ under libfyaml.
test/psych/test_omap.rbSkips tag-format assertion under libfyaml.
test/psych/test_json_tree.rbSkips JSON flow formatting assertions under libfyaml.
test/psych/test_exception.rbAdjusts exception context expectation (libfyaml diagnostics differ).
test/psych/test_encoding.rbSkips encoding-related expectations where libfyaml errors/tags differ.
test/psych/test_data.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_coder.rbSkips formatting/tag-synthesis expectations under libfyaml.
test/psych/json/test_stream.rbSkips JSON flow formatting assertions under libfyaml.
README.mdDocuments the experimental libfyaml backend, build flag, behavior differences, and backend detection.
.github/workflows/libfyaml.ymlAdds CI job that builds with --enable-libfyaml, verifies backend, and runs tests on Linux/macOS.

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

Comment threadext/psych/psych.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadtest/psych/test_string.rb
Comment threadext/psych/psych.c
Comment threadext/psych/psych_parser_fy.c Outdated
hsbtand others added 14 commits July 1, 2026 15:25
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a libfyaml? helper and guard the tests that intentionally diverge on
the experimental YAML 1.2 backend: yes/no/on/off are strings rather than
booleans, flow collections and block scalars are formatted differently,
tags and marks are reported differently, and non-ASCII tags/aliases are
rejected. test_boolean asserts the 1.2 string result directly; the
formatting and mark cases are skipped. The default libyaml build is
unaffected (every guard keys off Psych::BACKEND).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover what the libfyaml backend does distinctly, each placed with the
concern it belongs to: the Psych::BACKEND and Psych.libfyaml_version
checks in test_psych.rb, the YAML 1.2 "Norway problem" boolean case in
test_boolean.rb, and the "1.1 booleans are not quoted" emission case in
test_string.rb. The 1.2 assertions are skipped on the default libyaml
backend so the same suite passes under both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build psych with --enable-libfyaml and run the suite on Linux and macOS
(libfyaml is not supported on Windows). A verification step asserts that
Psych::BACKEND is actually libfyaml so the job cannot silently fall back
to libyaml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain how to enable it (--enable-libfyaml), that it is unsupported on
Windows, the YAML 1.2 boolean behavior it brings (the "Norway problem"
fix), and how to check the active backend, with an experimental caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify command was a plain YAML scalar, so the " #{Psych::BACKEND}"
started a YAML comment and truncated the shell command, leaving an
unterminated quote. Use a block scalar so the interpolation survives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An adversarial review confirmed two backend-visible differences that were
not written down. Scalars emitted with the default style can be formatted
differently from libyaml, and Psych::SyntaxError#context is always nil
because libfyaml keeps the whole diagnostic in #problem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the rough parse/emit benchmark so users pick the backend on purpose:
libfyaml is for YAML 1.2 semantics, and libyaml stays the choice when emit
throughput matters since libfyaml dumps about 1.7x to 1.9x slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hsbt
hsbtforce-pushed the claude/cool-carson-18ae10 branch from 68098bf to d185ff2CompareJuly 1, 2026 06:35

@tenderlovetenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. I think we should eventually remove the omit calls, but I think this PR is great. Thank you for doing this!!

@hsbt

hsbt commented Jul 2, 2026

Copy link
Copy Markdown
MemberAuthor

@tenderlove Thanks for your looking. I forgot to mention performance resulst in this pull request. I copy-and-paste from README.md update.

This backend targets YAML 1.2 compliance, not speed. In a rough
single-machine benchmark that loads and dumps in-memory documents, parsing
was roughly on par with libyaml (sometimes faster on string-heavy input),
while emitting was about 1.7x to 1.9x slower.

I'm not familiar with C code. But I did buttle-reviewed by three Claude subagents with API/Security/UX forcuses and confirmed them.

@hsbt
hsbt merged commit 7d5e036 into masterJul 2, 2026
194 checks passed
@hsbt
hsbt deleted the claude/cool-carson-18ae10 branch July 2, 2026 02:39
peterzhu2118 added a commit to ruby/ruby that referenced this pull request Jul 2, 2026
ruby/psych#805 added files psych_emitter_fy.c and
psych_parser_fy.c so we need to update the depend file too.
hsbt added a commit to ruby/ruby that referenced this pull request Aug 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@hsbt@tenderlove
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add an experimental opt-in libfyaml backend - #805

Merged
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10
Jul 2, 2026
Merged

Add an experimental opt-in libfyaml backend#805
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10

Conversation

@hsbt

@hsbthsbt commented Jul 1, 2026

Copy link
Copy Markdown
Member

Psych is tied to libyaml, which implements YAML 1.1 and rejects some valid YAML 1.2 documents. This adds an experimental alternative backend built on libfyaml, a fully YAML 1.2 compliant parser and emitter, compiled only when psych is built with --enable-libfyaml. Without the flag nothing changes and the default libyaml backend is used, and the flag is rejected on Windows where libfyaml is unsupported.

Because libfyaml follows YAML 1.2, the YAML 1.1 booleans yes, no, on, and off load as plain strings rather than true or false, which resolves the "Norway problem" where the country code no became false. This is keyed on Psych::BACKEND, so the libyaml default keeps its current behavior.

The parser and emitter are reimplemented against libfyaml's event API in translation units guarded by PSYCH_USE_LIBFYAML. Errors and positions are recovered from libfyaml's diagnostics, UTF-16 IO is transcoded to UTF-8, and scalar emission is matched to libyaml where it matters. The test suite is made backend-aware and gains positive coverage for the libfyaml behavior, and a new CI workflow builds with the flag on Linux and macOS and asserts the active backend before running the suite.

The backend is experimental. Its output is valid YAML but is formatted differently from libyaml in places and a few emitter edge cases are not yet matched, so libyaml remains the supported default.

hsbtand others added 2 commits July 1, 2026 06:48
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings July 1, 2026 02:04

CopilotAI 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

Adds an experimental, opt-in libfyaml backend to Psych to enable YAML 1.2–compliant parsing/emitting when built with --enable-libfyaml, while keeping the existing libyaml backend as the default. The change introduces new libfyaml-backed C implementations for the parser/emitter, exposes backend identification/version info, updates boolean/scalar scanning for YAML 1.2 semantics, and adjusts the test suite + CI to be backend-aware.

Changes:

  • Add PSYCH_USE_LIBFYAML build mode (--enable-libfyaml) with libfyaml-backed parser/emitter translation units and Psych::BACKEND / Psych.libfyaml_version.
  • Update scalar scanning and tests to reflect YAML 1.2 core schema behavior (notably yes/no/on/off as strings).
  • Add a dedicated GitHub Actions workflow to compile and run the suite with the libfyaml backend on Linux/macOS.

Reviewed changes

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

Show a summary per file
FileDescription
ext/psych/extconf.rbAdds --enable-libfyaml build option, pkg-config detection, and Windows rejection.
ext/psych/psych.hSwitches header include between yaml.h and libfyaml.h based on PSYCH_USE_LIBFYAML.
ext/psych/psych.cExposes Psych::BACKEND; adds Psych.libfyaml_version; adapts version reporting under libfyaml.
ext/psych/psych_parser.cCompiles out the libyaml parser when building with libfyaml.
ext/psych/psych_parser_fy.cNew libfyaml-backed parser implementation using libfyaml’s event API.
ext/psych/psych_emitter.cCompiles out the libyaml emitter when building with libfyaml.
ext/psych/psych_emitter_fy.cNew libfyaml-backed emitter implementation mapping Psych events to libfyaml emission.
lib/psych/scalar_scanner.rbMakes boolean scanning backend-aware (YAML 1.1 vs 1.2 boolean sets).
test/psych/helper.rbAdds libfyaml? helper for backend-aware tests.
test/psych/test_boolean.rbUpdates boolean tests to reflect YAML 1.2 behavior and adds Norway-problem coverage.
test/psych/test_psych.rbAdds backend/version assertions and skips unsupported/format-different cases under libfyaml.
test/psych/test_string.rbSkips YAML 1.1 boolean quoting assertions under libfyaml; adds libfyaml-specific expectations.
test/psych/visitors/test_to_ruby.rbSkips YAML 1.1 yes/no/on/off boolean visitor tests under libfyaml.
test/psych/test_yaml.rbSkips a YAML-spec fixture that depends on YAML 1.1 boolean typing under libfyaml.
test/psych/test_yaml_special_cases.rbSkips YAML 1.1 off boolean special-case under libfyaml.
test/psych/test_tree_builder.rbSkips location assertions that differ under libfyaml.
test/psych/test_symbol.rbSkips a known libfyaml emitter round-trip limitation for indicator-named symbols.
test/psych/test_set.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_parser.rbSkips mark position assertions that differ under libfyaml.
test/psych/test_omap.rbSkips tag-format assertion under libfyaml.
test/psych/test_json_tree.rbSkips JSON flow formatting assertions under libfyaml.
test/psych/test_exception.rbAdjusts exception context expectation (libfyaml diagnostics differ).
test/psych/test_encoding.rbSkips encoding-related expectations where libfyaml errors/tags differ.
test/psych/test_data.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_coder.rbSkips formatting/tag-synthesis expectations under libfyaml.
test/psych/json/test_stream.rbSkips JSON flow formatting assertions under libfyaml.
README.mdDocuments the experimental libfyaml backend, build flag, behavior differences, and backend detection.
.github/workflows/libfyaml.ymlAdds CI job that builds with --enable-libfyaml, verifies backend, and runs tests on Linux/macOS.

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

Comment threadext/psych/psych.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadtest/psych/test_string.rb
Comment threadext/psych/psych.c
Comment threadext/psych/psych_parser_fy.c Outdated
hsbtand others added 14 commits July 1, 2026 15:25
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a libfyaml? helper and guard the tests that intentionally diverge on
the experimental YAML 1.2 backend: yes/no/on/off are strings rather than
booleans, flow collections and block scalars are formatted differently,
tags and marks are reported differently, and non-ASCII tags/aliases are
rejected. test_boolean asserts the 1.2 string result directly; the
formatting and mark cases are skipped. The default libyaml build is
unaffected (every guard keys off Psych::BACKEND).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover what the libfyaml backend does distinctly, each placed with the
concern it belongs to: the Psych::BACKEND and Psych.libfyaml_version
checks in test_psych.rb, the YAML 1.2 "Norway problem" boolean case in
test_boolean.rb, and the "1.1 booleans are not quoted" emission case in
test_string.rb. The 1.2 assertions are skipped on the default libyaml
backend so the same suite passes under both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build psych with --enable-libfyaml and run the suite on Linux and macOS
(libfyaml is not supported on Windows). A verification step asserts that
Psych::BACKEND is actually libfyaml so the job cannot silently fall back
to libyaml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain how to enable it (--enable-libfyaml), that it is unsupported on
Windows, the YAML 1.2 boolean behavior it brings (the "Norway problem"
fix), and how to check the active backend, with an experimental caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify command was a plain YAML scalar, so the " #{Psych::BACKEND}"
started a YAML comment and truncated the shell command, leaving an
unterminated quote. Use a block scalar so the interpolation survives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An adversarial review confirmed two backend-visible differences that were
not written down. Scalars emitted with the default style can be formatted
differently from libyaml, and Psych::SyntaxError#context is always nil
because libfyaml keeps the whole diagnostic in #problem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the rough parse/emit benchmark so users pick the backend on purpose:
libfyaml is for YAML 1.2 semantics, and libyaml stays the choice when emit
throughput matters since libfyaml dumps about 1.7x to 1.9x slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hsbt
hsbtforce-pushed the claude/cool-carson-18ae10 branch from 68098bf to d185ff2CompareJuly 1, 2026 06:35

@tenderlovetenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. I think we should eventually remove the omit calls, but I think this PR is great. Thank you for doing this!!

@hsbt

hsbt commented Jul 2, 2026

Copy link
Copy Markdown
MemberAuthor

@tenderlove Thanks for your looking. I forgot to mention performance resulst in this pull request. I copy-and-paste from README.md update.

This backend targets YAML 1.2 compliance, not speed. In a rough
single-machine benchmark that loads and dumps in-memory documents, parsing
was roughly on par with libyaml (sometimes faster on string-heavy input),
while emitting was about 1.7x to 1.9x slower.

I'm not familiar with C code. But I did buttle-reviewed by three Claude subagents with API/Security/UX forcuses and confirmed them.

@hsbt
hsbt merged commit 7d5e036 into masterJul 2, 2026
194 checks passed
@hsbt
hsbt deleted the claude/cool-carson-18ae10 branch July 2, 2026 02:39
peterzhu2118 added a commit to ruby/ruby that referenced this pull request Jul 2, 2026
ruby/psych#805 added files psych_emitter_fy.c and
psych_parser_fy.c so we need to update the depend file too.
hsbt added a commit to ruby/ruby that referenced this pull request Aug 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@hsbt@tenderlove
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add an experimental opt-in libfyaml backend - #805

Merged
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10
Jul 2, 2026
Merged

Add an experimental opt-in libfyaml backend#805
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10

Conversation

@hsbt

@hsbthsbt commented Jul 1, 2026

Copy link
Copy Markdown
Member

Psych is tied to libyaml, which implements YAML 1.1 and rejects some valid YAML 1.2 documents. This adds an experimental alternative backend built on libfyaml, a fully YAML 1.2 compliant parser and emitter, compiled only when psych is built with --enable-libfyaml. Without the flag nothing changes and the default libyaml backend is used, and the flag is rejected on Windows where libfyaml is unsupported.

Because libfyaml follows YAML 1.2, the YAML 1.1 booleans yes, no, on, and off load as plain strings rather than true or false, which resolves the "Norway problem" where the country code no became false. This is keyed on Psych::BACKEND, so the libyaml default keeps its current behavior.

The parser and emitter are reimplemented against libfyaml's event API in translation units guarded by PSYCH_USE_LIBFYAML. Errors and positions are recovered from libfyaml's diagnostics, UTF-16 IO is transcoded to UTF-8, and scalar emission is matched to libyaml where it matters. The test suite is made backend-aware and gains positive coverage for the libfyaml behavior, and a new CI workflow builds with the flag on Linux and macOS and asserts the active backend before running the suite.

The backend is experimental. Its output is valid YAML but is formatted differently from libyaml in places and a few emitter edge cases are not yet matched, so libyaml remains the supported default.

hsbtand others added 2 commits July 1, 2026 06:48
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings July 1, 2026 02:04

CopilotAI 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

Adds an experimental, opt-in libfyaml backend to Psych to enable YAML 1.2–compliant parsing/emitting when built with --enable-libfyaml, while keeping the existing libyaml backend as the default. The change introduces new libfyaml-backed C implementations for the parser/emitter, exposes backend identification/version info, updates boolean/scalar scanning for YAML 1.2 semantics, and adjusts the test suite + CI to be backend-aware.

Changes:

  • Add PSYCH_USE_LIBFYAML build mode (--enable-libfyaml) with libfyaml-backed parser/emitter translation units and Psych::BACKEND / Psych.libfyaml_version.
  • Update scalar scanning and tests to reflect YAML 1.2 core schema behavior (notably yes/no/on/off as strings).
  • Add a dedicated GitHub Actions workflow to compile and run the suite with the libfyaml backend on Linux/macOS.

Reviewed changes

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

Show a summary per file
FileDescription
ext/psych/extconf.rbAdds --enable-libfyaml build option, pkg-config detection, and Windows rejection.
ext/psych/psych.hSwitches header include between yaml.h and libfyaml.h based on PSYCH_USE_LIBFYAML.
ext/psych/psych.cExposes Psych::BACKEND; adds Psych.libfyaml_version; adapts version reporting under libfyaml.
ext/psych/psych_parser.cCompiles out the libyaml parser when building with libfyaml.
ext/psych/psych_parser_fy.cNew libfyaml-backed parser implementation using libfyaml’s event API.
ext/psych/psych_emitter.cCompiles out the libyaml emitter when building with libfyaml.
ext/psych/psych_emitter_fy.cNew libfyaml-backed emitter implementation mapping Psych events to libfyaml emission.
lib/psych/scalar_scanner.rbMakes boolean scanning backend-aware (YAML 1.1 vs 1.2 boolean sets).
test/psych/helper.rbAdds libfyaml? helper for backend-aware tests.
test/psych/test_boolean.rbUpdates boolean tests to reflect YAML 1.2 behavior and adds Norway-problem coverage.
test/psych/test_psych.rbAdds backend/version assertions and skips unsupported/format-different cases under libfyaml.
test/psych/test_string.rbSkips YAML 1.1 boolean quoting assertions under libfyaml; adds libfyaml-specific expectations.
test/psych/visitors/test_to_ruby.rbSkips YAML 1.1 yes/no/on/off boolean visitor tests under libfyaml.
test/psych/test_yaml.rbSkips a YAML-spec fixture that depends on YAML 1.1 boolean typing under libfyaml.
test/psych/test_yaml_special_cases.rbSkips YAML 1.1 off boolean special-case under libfyaml.
test/psych/test_tree_builder.rbSkips location assertions that differ under libfyaml.
test/psych/test_symbol.rbSkips a known libfyaml emitter round-trip limitation for indicator-named symbols.
test/psych/test_set.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_parser.rbSkips mark position assertions that differ under libfyaml.
test/psych/test_omap.rbSkips tag-format assertion under libfyaml.
test/psych/test_json_tree.rbSkips JSON flow formatting assertions under libfyaml.
test/psych/test_exception.rbAdjusts exception context expectation (libfyaml diagnostics differ).
test/psych/test_encoding.rbSkips encoding-related expectations where libfyaml errors/tags differ.
test/psych/test_data.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_coder.rbSkips formatting/tag-synthesis expectations under libfyaml.
test/psych/json/test_stream.rbSkips JSON flow formatting assertions under libfyaml.
README.mdDocuments the experimental libfyaml backend, build flag, behavior differences, and backend detection.
.github/workflows/libfyaml.ymlAdds CI job that builds with --enable-libfyaml, verifies backend, and runs tests on Linux/macOS.

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

Comment threadext/psych/psych.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadtest/psych/test_string.rb
Comment threadext/psych/psych.c
Comment threadext/psych/psych_parser_fy.c Outdated
hsbtand others added 14 commits July 1, 2026 15:25
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a libfyaml? helper and guard the tests that intentionally diverge on
the experimental YAML 1.2 backend: yes/no/on/off are strings rather than
booleans, flow collections and block scalars are formatted differently,
tags and marks are reported differently, and non-ASCII tags/aliases are
rejected. test_boolean asserts the 1.2 string result directly; the
formatting and mark cases are skipped. The default libyaml build is
unaffected (every guard keys off Psych::BACKEND).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover what the libfyaml backend does distinctly, each placed with the
concern it belongs to: the Psych::BACKEND and Psych.libfyaml_version
checks in test_psych.rb, the YAML 1.2 "Norway problem" boolean case in
test_boolean.rb, and the "1.1 booleans are not quoted" emission case in
test_string.rb. The 1.2 assertions are skipped on the default libyaml
backend so the same suite passes under both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build psych with --enable-libfyaml and run the suite on Linux and macOS
(libfyaml is not supported on Windows). A verification step asserts that
Psych::BACKEND is actually libfyaml so the job cannot silently fall back
to libyaml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain how to enable it (--enable-libfyaml), that it is unsupported on
Windows, the YAML 1.2 boolean behavior it brings (the "Norway problem"
fix), and how to check the active backend, with an experimental caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify command was a plain YAML scalar, so the " #{Psych::BACKEND}"
started a YAML comment and truncated the shell command, leaving an
unterminated quote. Use a block scalar so the interpolation survives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An adversarial review confirmed two backend-visible differences that were
not written down. Scalars emitted with the default style can be formatted
differently from libyaml, and Psych::SyntaxError#context is always nil
because libfyaml keeps the whole diagnostic in #problem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the rough parse/emit benchmark so users pick the backend on purpose:
libfyaml is for YAML 1.2 semantics, and libyaml stays the choice when emit
throughput matters since libfyaml dumps about 1.7x to 1.9x slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hsbt
hsbtforce-pushed the claude/cool-carson-18ae10 branch from 68098bf to d185ff2CompareJuly 1, 2026 06:35

@tenderlovetenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. I think we should eventually remove the omit calls, but I think this PR is great. Thank you for doing this!!

@hsbt

hsbt commented Jul 2, 2026

Copy link
Copy Markdown
MemberAuthor

@tenderlove Thanks for your looking. I forgot to mention performance resulst in this pull request. I copy-and-paste from README.md update.

This backend targets YAML 1.2 compliance, not speed. In a rough
single-machine benchmark that loads and dumps in-memory documents, parsing
was roughly on par with libyaml (sometimes faster on string-heavy input),
while emitting was about 1.7x to 1.9x slower.

I'm not familiar with C code. But I did buttle-reviewed by three Claude subagents with API/Security/UX forcuses and confirmed them.

@hsbt
hsbt merged commit 7d5e036 into masterJul 2, 2026
194 checks passed
@hsbt
hsbt deleted the claude/cool-carson-18ae10 branch July 2, 2026 02:39
peterzhu2118 added a commit to ruby/ruby that referenced this pull request Jul 2, 2026
ruby/psych#805 added files psych_emitter_fy.c and
psych_parser_fy.c so we need to update the depend file too.
hsbt added a commit to ruby/ruby that referenced this pull request Aug 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@hsbt@tenderlove
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add an experimental opt-in libfyaml backend - #805

Merged
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10
Jul 2, 2026
Merged

Add an experimental opt-in libfyaml backend#805
hsbt merged 16 commits into
masterfrom
claude/cool-carson-18ae10

Conversation

@hsbt

@hsbthsbt commented Jul 1, 2026

Copy link
Copy Markdown
Member

Psych is tied to libyaml, which implements YAML 1.1 and rejects some valid YAML 1.2 documents. This adds an experimental alternative backend built on libfyaml, a fully YAML 1.2 compliant parser and emitter, compiled only when psych is built with --enable-libfyaml. Without the flag nothing changes and the default libyaml backend is used, and the flag is rejected on Windows where libfyaml is unsupported.

Because libfyaml follows YAML 1.2, the YAML 1.1 booleans yes, no, on, and off load as plain strings rather than true or false, which resolves the "Norway problem" where the country code no became false. This is keyed on Psych::BACKEND, so the libyaml default keeps its current behavior.

The parser and emitter are reimplemented against libfyaml's event API in translation units guarded by PSYCH_USE_LIBFYAML. Errors and positions are recovered from libfyaml's diagnostics, UTF-16 IO is transcoded to UTF-8, and scalar emission is matched to libyaml where it matters. The test suite is made backend-aware and gains positive coverage for the libfyaml behavior, and a new CI workflow builds with the flag on Linux and macOS and asserts the active backend before running the suite.

The backend is experimental. Its output is valid YAML but is formatted differently from libyaml in places and a few emitter edge cases are not yet matched, so libyaml remains the supported default.

hsbtand others added 2 commits July 1, 2026 06:48
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings July 1, 2026 02:04

CopilotAI 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

Adds an experimental, opt-in libfyaml backend to Psych to enable YAML 1.2–compliant parsing/emitting when built with --enable-libfyaml, while keeping the existing libyaml backend as the default. The change introduces new libfyaml-backed C implementations for the parser/emitter, exposes backend identification/version info, updates boolean/scalar scanning for YAML 1.2 semantics, and adjusts the test suite + CI to be backend-aware.

Changes:

  • Add PSYCH_USE_LIBFYAML build mode (--enable-libfyaml) with libfyaml-backed parser/emitter translation units and Psych::BACKEND / Psych.libfyaml_version.
  • Update scalar scanning and tests to reflect YAML 1.2 core schema behavior (notably yes/no/on/off as strings).
  • Add a dedicated GitHub Actions workflow to compile and run the suite with the libfyaml backend on Linux/macOS.

Reviewed changes

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

Show a summary per file
FileDescription
ext/psych/extconf.rbAdds --enable-libfyaml build option, pkg-config detection, and Windows rejection.
ext/psych/psych.hSwitches header include between yaml.h and libfyaml.h based on PSYCH_USE_LIBFYAML.
ext/psych/psych.cExposes Psych::BACKEND; adds Psych.libfyaml_version; adapts version reporting under libfyaml.
ext/psych/psych_parser.cCompiles out the libyaml parser when building with libfyaml.
ext/psych/psych_parser_fy.cNew libfyaml-backed parser implementation using libfyaml’s event API.
ext/psych/psych_emitter.cCompiles out the libyaml emitter when building with libfyaml.
ext/psych/psych_emitter_fy.cNew libfyaml-backed emitter implementation mapping Psych events to libfyaml emission.
lib/psych/scalar_scanner.rbMakes boolean scanning backend-aware (YAML 1.1 vs 1.2 boolean sets).
test/psych/helper.rbAdds libfyaml? helper for backend-aware tests.
test/psych/test_boolean.rbUpdates boolean tests to reflect YAML 1.2 behavior and adds Norway-problem coverage.
test/psych/test_psych.rbAdds backend/version assertions and skips unsupported/format-different cases under libfyaml.
test/psych/test_string.rbSkips YAML 1.1 boolean quoting assertions under libfyaml; adds libfyaml-specific expectations.
test/psych/visitors/test_to_ruby.rbSkips YAML 1.1 yes/no/on/off boolean visitor tests under libfyaml.
test/psych/test_yaml.rbSkips a YAML-spec fixture that depends on YAML 1.1 boolean typing under libfyaml.
test/psych/test_yaml_special_cases.rbSkips YAML 1.1 off boolean special-case under libfyaml.
test/psych/test_tree_builder.rbSkips location assertions that differ under libfyaml.
test/psych/test_symbol.rbSkips a known libfyaml emitter round-trip limitation for indicator-named symbols.
test/psych/test_set.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_parser.rbSkips mark position assertions that differ under libfyaml.
test/psych/test_omap.rbSkips tag-format assertion under libfyaml.
test/psych/test_json_tree.rbSkips JSON flow formatting assertions under libfyaml.
test/psych/test_exception.rbAdjusts exception context expectation (libfyaml diagnostics differ).
test/psych/test_encoding.rbSkips encoding-related expectations where libfyaml errors/tags differ.
test/psych/test_data.rbSkips strict dump-format assertion under libfyaml.
test/psych/test_coder.rbSkips formatting/tag-synthesis expectations under libfyaml.
test/psych/json/test_stream.rbSkips JSON flow formatting assertions under libfyaml.
README.mdDocuments the experimental libfyaml backend, build flag, behavior differences, and backend detection.
.github/workflows/libfyaml.ymlAdds CI job that builds with --enable-libfyaml, verifies backend, and runs tests on Linux/macOS.

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

Comment threadext/psych/psych.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadext/psych/psych_emitter_fy.c
Comment threadtest/psych/test_string.rb
Comment threadext/psych/psych.c
Comment threadext/psych/psych_parser_fy.c Outdated
hsbtand others added 14 commits July 1, 2026 15:25
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a libfyaml? helper and guard the tests that intentionally diverge on
the experimental YAML 1.2 backend: yes/no/on/off are strings rather than
booleans, flow collections and block scalars are formatted differently,
tags and marks are reported differently, and non-ASCII tags/aliases are
rejected. test_boolean asserts the 1.2 string result directly; the
formatting and mark cases are skipped. The default libyaml build is
unaffected (every guard keys off Psych::BACKEND).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover what the libfyaml backend does distinctly, each placed with the
concern it belongs to: the Psych::BACKEND and Psych.libfyaml_version
checks in test_psych.rb, the YAML 1.2 "Norway problem" boolean case in
test_boolean.rb, and the "1.1 booleans are not quoted" emission case in
test_string.rb. The 1.2 assertions are skipped on the default libyaml
backend so the same suite passes under both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build psych with --enable-libfyaml and run the suite on Linux and macOS
(libfyaml is not supported on Windows). A verification step asserts that
Psych::BACKEND is actually libfyaml so the job cannot silently fall back
to libyaml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain how to enable it (--enable-libfyaml), that it is unsupported on
Windows, the YAML 1.2 boolean behavior it brings (the "Norway problem"
fix), and how to check the active backend, with an experimental caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify command was a plain YAML scalar, so the " #{Psych::BACKEND}"
started a YAML comment and truncated the shell command, leaving an
unterminated quote. Use a block scalar so the interpolation survives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An adversarial review confirmed two backend-visible differences that were
not written down. Scalars emitted with the default style can be formatted
differently from libyaml, and Psych::SyntaxError#context is always nil
because libfyaml keeps the whole diagnostic in #problem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the rough parse/emit benchmark so users pick the backend on purpose:
libfyaml is for YAML 1.2 semantics, and libyaml stays the choice when emit
throughput matters since libfyaml dumps about 1.7x to 1.9x slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hsbt
hsbtforce-pushed the claude/cool-carson-18ae10 branch from 68098bf to d185ff2CompareJuly 1, 2026 06:35

@tenderlovetenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. I think we should eventually remove the omit calls, but I think this PR is great. Thank you for doing this!!

@hsbt

hsbt commented Jul 2, 2026

Copy link
Copy Markdown
MemberAuthor

@tenderlove Thanks for your looking. I forgot to mention performance resulst in this pull request. I copy-and-paste from README.md update.

This backend targets YAML 1.2 compliance, not speed. In a rough
single-machine benchmark that loads and dumps in-memory documents, parsing
was roughly on par with libyaml (sometimes faster on string-heavy input),
while emitting was about 1.7x to 1.9x slower.

I'm not familiar with C code. But I did buttle-reviewed by three Claude subagents with API/Security/UX forcuses and confirmed them.

@hsbt
hsbt merged commit 7d5e036 into masterJul 2, 2026
194 checks passed
@hsbt
hsbt deleted the claude/cool-carson-18ae10 branch July 2, 2026 02:39
peterzhu2118 added a commit to ruby/ruby that referenced this pull request Jul 2, 2026
ruby/psych#805 added files psych_emitter_fy.c and
psych_parser_fy.c so we need to update the depend file too.
hsbt added a commit to ruby/ruby that referenced this pull request Aug 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@hsbt@tenderlove