Skip to content

Add MatchData#integer_at - #9767

Open
zev wants to merge 1 commit into
jruby:ruby-4.1from
zev:matchdata-integer-at
Open

zev wants to merge 1 commit into
jruby:ruby-4.1from
zev:matchdata-integer-at

Conversation

@zev

@zev zev commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Implements MatchData#integer_at from Ruby 4.1 (Feature #21932), part of #9746.

Converts the matched substring to an Integer like String#to_i, but returns nil when nothing could be parsed, as CRuby does via rb_int_parse_cstr.

  • RubyMatchData: integer_at, ported from match_integer_at (index/name resolution, base validation, negative indexes)
  • ConvertBytes: new byteListToInumOrNil, which parses in place and returns nil only when CRuby does. Existing to_i/Integer() behavior is unchanged.
  • nameToBackrefError: adds the missing colon so the message matches CRuby ("undefined group name reference: x"). This also affects values_at/byteoffset with a String-pattern match.

Testing

  • spec/ruby/core/matchdata/integer_at_spec.rb passes in default, interpreter, and forced JIT modes.
  • Related specs (MatchData, String#to_i, Integer(), to_r, to_c, StringScanner) pass.
  • rake spec:ruby:fast compared against unmodified ruby-4.1: no new failures.
  • Compared 83 edge cases against a CRuby 4.1.0dev build (whitespace, signs, underscores, prefixes with each base, invalid radixes, named and negative indexes, UTF-16/32 and Shift_JIS captures). All match except the one below.

Note: CRuby vs. the agreed behavior in Feature #21932

This PR matches CRuby 4.1 and the current ruby/spec, but both differ from the behavior agreed in the feature discussion. Note 7 (recording Matz's decision) says integer_at should behave like $N&.to_i, returning nil only when the group did not match:

Capture Note 7 CRuby 4.1 / this PR
"foo" 0 nil
"" (empty) 0 nil
unmatched group nil nil

CRuby's implementation (72eb59d0b2) predates note 7 and uses rb_int_parse_cstr, which returns nil when nothing was parsed (so "T" is nil but "-T" is 0). The ruby/spec example "returns nil on non-integer matches" encodes that, and CRuby's StringScanner#integer_at also returns nil for an empty capture. CRuby's own rdoc still says it is equivalent to $N&.to_i.

Decision: this PR follows CRuby, and we'll raise the discrepancy upstream for clarification (see discussion below). If CRuby moves to the to_i behavior, a small follow-up here will switch integer_at to the existing to_i parsing and remove ConvertBytes.byteListToInumOrNil.

Follow-up (not in this PR)

integer_at(0, nil) raises TypeError: no implicit conversion from nil to integer, while CRuby 4.1 says no implicit conversion of nil into Integer. This is from the 4.1 TypeError wording change (Bug #21864) and affects every nil-to-Integer conversion, not just this method. The messages are produced at:

The specs already expect the new wording via raise_consistent_error and are currently tagged. I'm happy to do this as a separate PR if you'd like it tracked in #9746.

🤖 Generated with Claude Code

Converts the matched substring to an Integer like String#to_i, but
returns nil when nothing could be parsed, matching CRuby's use of
rb_int_parse_cstr. ConvertBytes gains byteListToInumOrNil for this.

Also add the missing colon to the "undefined group name reference"
IndexError raised by nameToBackrefError, matching CRuby.

See https://bugs.ruby-lang.org/issues/21932
Part of jruby#9746

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread core/src/main/java/org/jruby/RubyMatchData.java
}

/**
* MRI: match_integer_at

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see different styles for this in this file, is there a specific one I should follow like

/** match_integer_at
 *
 */

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.

The format you have in place is fine. We probably should introduce an annotation for associating our methods with their CRuby equivalents, so they can be audited by tools and included in docs. Has been a TODO for me for a while.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I went ahead and did a quick spike on brainstorming a possible annotation of MRI or CRuby. Here is a proposal: On the annotation idea: here is a concrete proposal for linking a Java method to the CRuby C function it ports, followed by a survey of what migrating the existing comments would involve. I can move this to a separate issue if you'd rather not have it on this PR.

Proposal

An annotation in org.jruby.anno:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, CONSTRUCTOR, TYPE, FIELD})
@Repeatable(CRuby.List.class)
public @interface CRuby {
    String value();           // the C function or macro, e.g. "match_integer_at"
    String file() default ""; // e.g. "re.c"; optional, omitted when ambiguous
    String note() default ""; // e.g. "first half"; optional free text

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({METHOD, CONSTRUCTOR, TYPE, FIELD})
    @interface List { CRuby[] value(); }
}

@CRuby("match_size") works as the short form when there is nothing else to say.

Open questions:

  • Name: @CRuby matches your wording. @MRI matches the comments we already have. I lean toward @CRuby, but the examples below work the same with @MRI, so I'm happy with either (the sketch and examples use @CRuby).
  • Fields: is file worth having? It makes the link auditable and lets docs link to the C source, but it has to be maintained. note exists so qualifiers like "first half" aren't lost.
  • Retention: I used RUNTIME to match the other org.jruby.anno annotations and @Documented so it shows up in Javadoc. CLASS would work too if you don't want it reflectable.

Before and after

This PR (integer_at)

Before:

/**
 * MRI: match_integer_at
 */
@JRubyMethod
public IRubyObject integer_at(ThreadContext context, IRubyObject idx) {

After:

@CRuby(value = "match_integer_at", file = "re.c")   // or @MRI(...) if we pick that name
@JRubyMethod
public IRubyObject integer_at(ThreadContext context, IRubyObject idx) {

Several C functions (RubyArrayNative#rejectBang)

Before:

// MRI: ary_reject_bang and reject_bang_i
public IRubyObject rejectBang(ThreadContext context, Block block) {

After (with @MRI instead, it would read @MRI(value = "ary_reject_bang", file = "array.c") and so on):

@CRuby(value = "ary_reject_bang", file = "array.c")
@CRuby(value = "reject_bang_i", file = "array.c")
public IRubyObject rejectBang(ThreadContext context, Block block) {

The bare /** match_size */ Javadoc form converts the same way, and qualifiers like "first half" (// MRI: rb_str_count, first half) go into note. Comments inside a method body, like // MRI: nurat_canonicalize, negation part in RubyRational, can't take the annotation. They stay as comments, or the helper they call (canonicalizeShouldNegate) can take the annotation instead.

Migration survey

I wrote a dry-run script to see what a migration would involve. It runs under JRuby, uses the JDK's javac Tree API to attach each comment to the declaration below it, and checks every name against a C-identifier index of a CRuby checkout. It does not modify any files.

core/src/main/java (1,560 files)

Category Count Meaning
clean 1,068 Comment above a declaration is just one C name, so it converts mechanically
multi 9 Several C names, so it needs the repeatable form
prose 100 C name plus a description. The annotation is added and the prose stays
qualified 90 C name plus free text ("first half", "sort of"). Needs human review
inner 33 Comment is inside a method body, so it can't take an annotation
orphan 4 Comment is not above any declaration
mention 353 Says "MRI" but names no C function ("MRI doesn't..."), so it is not a mapping
  • Mechanically convertible (clean + multi + prose): 1,177. Of those, 1,109 name only functions that exist in CRuby, and 1,065 have a single unambiguous source file.
  • Comment forms: of the 1,304 actual mappings, 741 (57%) are bare /** rb_foo */ Javadoc and 563 use MRI:, so most can't be found by grepping for MRI:.
  • Coverage: 370 of 3,874 @JRubyMethod methods (9.6%) have any C reference today.
  • Stale names: 79 references (78 distinct names) weren't found anywhere in the CRuby tree. For example rb_fix_truncate (RubyFixnum), rb_ary_subseq_step and rb_big_truncate have no hits in a grep of CRuby. These are likely renames or removals that a drift checker would surface.

The inner and orphan cases are about 4% of the total and would probably stay as comments.

Possible next steps

  1. Agree on the annotation's name and fields.
  2. Turn the script into a real codemod with an --apply mode, run one package per PR. I'm happy to do this if the design looks right.
  3. Possibly add a build-time manifest (Java method to C function) and a CI check that the C names still exist in the CRuby version we target.

@zev

zev commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

@headius Before I mark this ready, I'd like your take on the "Open question" section in the description. CRuby's integer_at returns nil for non-numeric and empty captures, but the agreed behavior in Feature #21932 note 7 is $N&.to_i (so 0). This PR currently matches CRuby and ruby/spec.

Note 7's own examples, run on CRuby 4.1.0dev and this PR:

Example Note 7 CRuby 4.1 JRuby (this PR)
"2024", bases 10 / 8 / 16 2024 / 1044 / 8228 same same
"0xF", base 10 / base 0 0 / 15 same same
"1_0_0" 100 100 100
Unmatched group nil nil nil
"foo" 0 nil nil
"" (empty capture) 0 nil nil

And the five examples in spec/ruby/core/matchdata/integer_at_spec.rb:

Spec example Agrees with note 7?
converts matches, including leading zeros ("03" → 3) ✓
"returns nil on non-matching index matches" (unmatched group) ✓
"returns nil on non-integer matches" ("T" → nil) ✗ Note 7 says 0. This example records the implementation's behavior.
base conversion ("0c" → 0 in base 10, 12 in base 16) ✓
base 0 detects prefixes ✓

Should we keep matching CRuby, and do you think it's worth reporting upstream?

@headius

headius commented Oct 2, 2026

Copy link
Copy Markdown
Member

Should we keep matching CRuby here and report the discrepancy upstream?

Yeah it seems inconsistent so we can raise it upstream and at least get clarification. Pretty edge behavior in any case.

@headius headius added this to the JRuby 10.2.0.0 milestone Oct 2, 2026
@zev

zev commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Should I raise this or will you? In both cases where should we raise it? In the ruby bug tracker or a PR for the lang specs?

@zev
zev marked this pull request as ready for review October 2, 2026 23:54
@zev

zev commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Marking this ready for review. As discussed above, it follows CRuby 4.1 (nil for non-numeric and empty captures, matching ruby/spec). There may be a follow-up depending on the upstream answer about Feature #21932 note 7: if CRuby switches to $N&.to_i behavior, the JRuby change would be small (use the existing to_i parsing and drop ConvertBytes.byteListToInumOrNil). The nil-to-Integer TypeError wording noted inline will also be a separate PR.

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.

2 participants