Skip to content

feat(format): %c coercion, hash-default, hex-float #/0 fill, *n$ and width/precision overflow - #7

Merged
tannevaled merged 1 commit into
mainfrom
feat/sprintf-c-coerce-hexfloat-hooks
Aug 31, 2026
Merged

tannevaled merged 1 commit into
mainfrom
feat/sprintf-c-coerce-hexfloat-hooks

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

Adds the host hooks and directive-parsing fixes the rbgo binding needs to match MRI 4.0.6 sprintf / Kernel#format / String#% for cases the engine alone could not reach:

  • charCoercer (CoerceChar) — lets a host resolve a %c operand through its own to_str-then-to_int protocol; renderChar consults it before the Kind-based path.
  • NamedArgs.SetDefault — registers a resolver consulted when a %<name>/%{name} key is absent from the map, the hook through which a host applies Ruby's Hash#default / default_proc.
  • %#a / %#A — the alternate (#) form now forces the radix point on hex-float even when no fractional hex digit remains ("%#a" % 16.0 → "0x1.p+4").
  • %0…a zero-fill — a hex-float body is zero-filled after its 0x/0X prefix ("%020a" % 12.5 → "0x000…1.9p+3").
  • %*1$.*2$3$d — a n$ value reference may follow *-supplied width/precision; maybeArgNum records it before the verb is read.
  • width/precision overflow — a digit run too big for int raises MRI's width too big / precision too big ArgumentError.

Testing

GOWORK=off go test ./... green; 100.0% statement coverage preserved (golden corpus and oracle cases extended).

🤖 Generated with Claude Code

… *n$, width/precision overflow

Add the host hooks and directive-parsing fixes the rbgo binding needs to
match MRI 4.0.6 sprintf/String#% for cases the engine alone could not reach:

- charCoercer (CoerceChar) lets a host resolve a %c operand via its own
  to_str-then-to_int protocol; renderChar consults it before the Kind path.
- NamedArgs.SetDefault registers a resolver for a %<name>/%{name} key absent
  from the map, so a host can apply Ruby's Hash#default / default_proc.
- formatHexFloat honours the alternate (#) form, forcing the radix point on
  %a/%A even when no fractional hex digit remains ("%#a" of 16.0 -> "0x1.p+4").
- padFloat zero-fills a hex-float body after its "0x"/"0X" prefix
  ("%020a" -> "0x000...1.9p+3") instead of before it.
- A "n$" value reference may follow '*'-supplied width/precision, as in
  "%*1$.*2$3$d"; maybeArgNum records it before the verb is read.
- parseNumber reports overflow so a width/precision digit run too big for int
  raises MRI's "width too big" / "precision too big" ArgumentError.

Golden corpus and oracle cases extended; 100% statement coverage preserved.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit f58c7d1 into main Aug 31, 2026
11 checks passed
@tannevaled
tannevaled deleted the feat/sprintf-c-coerce-hexfloat-hooks branch August 31, 2026 11:55
tannevaled added a commit to go-embedded-ruby/ruby that referenced this pull request Aug 31, 2026
…l in sprintf (#522)

* Match MRI %c coercion, Hash#default named refs, and hex-float # / 0-fill in sprintf

Wire the format binding to the new go-ruby-format host hooks so
Kernel#sprintf / Kernel#format / String#% match MRI 4.0.6 for cases the
engine could not reach alone:

- formatValue.CoerceChar resolves a %c operand with MRI's to_str-then-to_int
  protocol via the method table (never #respond_to?, so a BasicObject that
  defines only #to_str is coerced): a String feeds first-character rendering,
  an Integer a code point, a non-String/#to_int result raises the matching
  "can't convert X into String/Integer" TypeError, and an object answering
  neither raises "no implicit conversion of X into Integer".
- formatNamedArgs registers a NamedArgs default resolver so a %<name>/%{name}
  reference to an absent key consults the hash's own #[] (Hash#default /
  default_proc): a non-nil default is used, a nil one raises MRI's KeyError. A
  present key (even a nil value) stays in the map and never hits the resolver.

The hex-float alternate-form (%#a) and zero-fill (%020a), the "%*1$.*2$3$d"
argument-numbered star width/precision, and the width/precision "too big"
ArgumentError are fixed in the engine (go-ruby-format/format#7) and land here
through the dependency bump.

core/string/modulo_spec: 252 -> 263 examples pass.

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

* Probe %c/to_ary coercion methods without sending #respond_to? to a BasicObject

String#% unpacked its right-hand side by asking respondsToDynamic whether the
object answered #to_ary, whose fallback *sends* #respond_to? — which a
BasicObject operand does not have, so `"%c" % basic_object_with_to_str` raised
NoMethodError instead of coercing. Add formatRespondsTo, which probes the
method table first and only dispatches #respond_to? (and thus
#respond_to_missing?) to an object that actually provides it, matching MRI's
rb_check_funcall. Use it for the %c to_str/to_int coercion and the to_ary
unpacking. Covers CoerceChar and the named-hash default resolver with a new
format_c_named_test.go.

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

* chore(deps): re-pin go-ruby-format/format to merged main

Engine PR #7 merged; repoint from the PR-branch pseudoversion to the
resulting main pseudoversion so the sprintf/format binding builds against
released engine code.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant