Skip to content

Conversation

@allcre
Copy link

@allcre allcre commented May 13, 2025

Ticket: https://github.com/Shopify/team-ruby-dx/issues/1453
This change implements lower bound constraints for generic type parameters in RBS, allowing declarations like [T > SomeType].

The implementation includes:

  • Parser and lexer modifications to handle ">" token in type params
  • Relevant changes to the Ruby API, like Locator and Validator
  • Schema updates to typeParam.json
  • Documentation updates

To review

  • My approach was to find how upper_bound is handled and duplicate the logic for lower_bound. Did I miss anything?
  • Any documentation updates missing?
  • Any missing tests?

note: CI is currently failing but this will be fixed by ruby#2482

src/parser.c Outdated
upper_bound_range.end = parser->current_token.range.end;
rbs_range_t lower_bound_range = NULL_RANGE;

for(int bound_parse_attempt = 0; bound_parse_attempt < 2; bound_parse_attempt++) {
Copy link
Author

Choose a reason for hiding this comment

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

This loop is to parse both upper bound (<) and lower bound (>) constraints for type parameters. The loop runs at most twice to allow both bound types in either order: [T < UpperBound > LowerBound] or [T > LowerBound < UpperBound]. It detects and prevents duplicate bounds of the same type and exits early if no bounds are present.

src/parser.c Outdated
Comment on lines 1452 to 1461
} else if (parser->next_token.type == pRBRACKET) {
break;
} else {
rbs_parser_set_error(parser, parser->next_token, true, "expected ',' or ']' after type parameter, got %s", rbs_token_type_str(parser->next_token.type));
return false;
Copy link
Author

Choose a reason for hiding this comment

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

This change ensures the parser correctly validates syntax after type parameter bounds. Previously, expressions like [T < String Integer] would be silently accepted despite being invalid.

@allcre allcre requested review from Morriar, amomchilov and st0012 May 13, 2025 17:00
Copy link

@amomchilov amomchilov left a comment

Choose a reason for hiding this comment

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

This is a really impressive piece of work!

You can also specify the _lower bound_ of the type parameter using `>`. This constrains the type parameter to be a supertype of the specified bound. Both an upper and a lower bound can be specified for the same type parameter.

```rbs
class FlexibleProcessor[T > Integer < Numeric]
Copy link

@amomchilov amomchilov May 13, 2025

Choose a reason for hiding this comment

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

Hmmmm I do not like this syntax... Integer < T < Numeric would be much better. 🤔 Let's discuss with @Morriar

Copy link
Author

Choose a reason for hiding this comment

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

Agreed that would be nicer. If only a lower bound was specified, would we want to accept Integer < T and not T > Integer?

Copy link

Choose a reason for hiding this comment

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

While it works well with one letter names mixed with core types, it can become more confusing when using semantic names:

SUV < Car < Vehicle

Which one is the type param?

Not that the original syntax is much better though...

Car > SUV < Vehicle 

I'm not against the proposed Integer < T < Numeric but it's still confusing when using a default type:

Integer < T < Numeric = Integer

In practice, lower bounds are not as used as upper bound and using both would be a really rare occasion. So I'm not sure any syntax matters much?

Copy link

Choose a reason for hiding this comment

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

We can ask Soutaro once we open the PR against ruby/rbs

@allcre allcre force-pushed the support-lower-bound branch from f5e12ef to 1a2750a Compare May 14, 2025 14:51
@allcre allcre requested a review from amomchilov May 14, 2025 14:56
@allcre allcre force-pushed the support-lower-bound branch from 1a2750a to 8abac1e Compare May 14, 2025 15:22
src/parser.c Outdated
upper_bound_range.end = parser->current_token.range.end;
rbs_range_t lower_bound_range = NULL_RANGE;

for (int bound_parse_attempt = 0; bound_parse_attempt < 2; bound_parse_attempt++) {
Copy link

Choose a reason for hiding this comment

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

smart 🤯

@allcre allcre force-pushed the support-lower-bound branch 2 times, most recently from d047a16 to 2e4b7bc Compare May 14, 2025 17:04
@allcre allcre requested a review from Morriar May 14, 2025 17:21
Comment on lines +163 to +166
void_type_context_validator(lb)
no_self_type_validator(lb)
no_classish_type_validator(lb)
@validator.validate_type(lb, context: nil)
Copy link
Member

Choose a reason for hiding this comment

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

Not directly related to this PR, but it feels like these 4 lines can be extracted into a helper method as it's repeated a lot 🤔

Comment on lines +130 to 133
[param.upper_bound_type, param.lower_bound_type].compact.each do |bound|
bound.free_variables.each do |tv|
block[tv]
end
Copy link
Member

Choose a reason for hiding this comment

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

Do you think this would be better?

Suggested change
[param.upper_bound_type, param.lower_bound_type].compact.each do |bound|
bound.free_variables.each do |tv|
block[tv]
end
[param.upper_bound_type, param.lower_bound_type].compact.flat_map(&:free_variables).each do |tv|
block[tv]
end

Copy link
Author

Choose a reason for hiding this comment

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

These are apparently not the same! With the suggested change, tests start failing, but I'm not exactly sure why.

@Morriar
Copy link

Morriar commented May 15, 2025

For the CI errors I think you need to bundle exec rake lexer and commits the changes

@st0012
Copy link
Member

st0012 commented May 15, 2025

For the CI errors I think you need to bundle exec rake lexer and commits the changes

I think it's caused by mismatches on the r2e versions. RBS currently uses v3.1 but we usually install 4.* now.

@allcre you can follow the same commands used on CI to install that specific version:

          cd /tmp
          curl -L https://github.com/skvadrik/re2c/archive/refs/tags/3.1.tar.gz > re2c-3.1.tar.gz
          tar xf re2c-3.1.tar.gz
          cd re2c-3.1
          autoreconf -i -W all
          ./configure
          make
          sudo make install

We can look at this together in our pairing later.

@allcre allcre force-pushed the support-lower-bound branch 4 times, most recently from 487f79c to c63e115 Compare May 15, 2025 16:32
This change implements lower bound constraints for generic type
parameters in RBS, allowing declarations like `[T > SomeType]`.

The implementation includes:
- Parser and lexer modifications to handle ">" token in type params
- Relevant changes to the Ruby API, like Locator and Validator
- Schema updates to typeParam.json
- Documentation updates
@allcre allcre force-pushed the support-lower-bound branch from c63e115 to ac8348a Compare May 20, 2025 16:03
@alexcrocha alexcrocha merged commit 2a73da6 into master Jul 8, 2025
22 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants