Skip to content

Commit 2a73da6

Browse files
authored
Merge pull request #2490 from Shopify/support-lower-bound
Add support for lower bounds in type parameters
2 parents 530f580 + ac8348a commit 2a73da6

File tree

24 files changed

+1708
-1329
lines changed

24 files changed

+1708
-1329
lines changed

config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ nodes:
279279
c_type: rbs_keyword
280280
- name: upper_bound
281281
c_type: rbs_node
282+
- name: lower_bound
283+
c_type: rbs_node
282284
- name: default_type
283285
c_type: rbs_node
284286
- name: unchecked

docs/syntax.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ _module-type-parameters_ ::= #
650650

651651
Class declaration can have type parameters and superclass. When you omit superclass, `::Object` is assumed.
652652

653-
* Super class arguments and generic class upperbounds are not *classish-context* nor *self-context*
653+
* Super class arguments and generic class bounds are not *classish-context* nor *self-context*
654654

655655
### Module declaration
656656

@@ -668,7 +668,7 @@ end
668668

669669
The `Enumerable` module above requires `each` method for enumerating objects.
670670

671-
* Self type arguments and generic class upperbounds are not *classish-context* nor *self-context*
671+
* Self type arguments and generic class bounds are not *classish-context* nor *self-context*
672672

673673
### Class/module alias declaration
674674

@@ -764,7 +764,11 @@ _module-type-parameter_ ::= _generics-unchecked_ _generics-variance_ _type-varia
764764
_method-type-param_ ::= _type-variable_ _generics-bound_
765765

766766
_generics-bound_ ::= (No type bound)
767-
| `<` _type_ (The generics parameter is bounded)
767+
| `<` _type_ (The generics parameter has an upper bound)
768+
| '>' _type_ (The generics parameter has a lower bound)
769+
770+
# A type parameter can have both upper and lower bounds, which can be specified in either order:
771+
# `[T < UpperBound > LowerBound]` or `[T > LowerBound < UpperBound]`
768772

769773
_default-type_ ::= (No default type)
770774
| `=` _type_ (The generics parameter has default type)
@@ -834,13 +838,38 @@ class PrettyPrint[T < _Output]
834838
end
835839
```
836840

837-
If a type parameter has an upper bound, the type parameter must be instantiated with types that is a subtype of the upper bound.
841+
If a type parameter has an upper bound, the type parameter must be instantiated with types that are a subtype of the upper bound.
838842

839843
```rbs
840844
type str_printer = PrettyPrint[String] # OK
841845
type int_printer = PrettyPrint[Integer] # Type error
842846
```
843847

848+
If a type parameter has a lower bound, the type parameter must be instantiated with types that are a supertype of the lower bound.
849+
850+
```rbs
851+
class PrettyPrint[T > Numeric]
852+
end
853+
854+
type obj_printer = PrettyPrint[Object] # OK
855+
type int_printer = PrettyPrint[Integer] # Type error
856+
```
857+
858+
A type parameter can have both an upper and a lower bound, and these bounds can be specified in any order.
859+
860+
```rbs
861+
class FlexibleProcessor[T > Integer < Numeric]
862+
# This class processes types T that are supertypes of Integer but also subtypes of Numeric.
863+
# This includes Integer, Rational, Complex, Float, and Numeric itself.
864+
def calculate: (T) -> T
865+
end
866+
867+
type int_processor = FlexibleProcessor[Integer] # OK (Integer > Integer and Integer < Numeric)
868+
type num_processor = FlexibleProcessor[Numeric] # OK (Numeric > Integer and Numeric < Numeric)
869+
type obj_processor = FlexibleProcessor[Object] # Type error (Object is not < Numeric)
870+
type str_processor = FlexibleProcessor[String] # Type error (String is not > Integer)
871+
```
872+
844873
The generics type parameter of modules, classes, interfaces, or type aliases can have a default type.
845874

846875
```rbs

ext/rbs_extension/ast_translation.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,7 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan
663663
rb_hash_aset(h, ID2SYM(rb_intern("name")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->name)); // rbs_ast_symbol
664664
rb_hash_aset(h, ID2SYM(rb_intern("variance")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->variance)); // rbs_keyword
665665
rb_hash_aset(h, ID2SYM(rb_intern("upper_bound")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->upper_bound)); // rbs_node
666+
rb_hash_aset(h, ID2SYM(rb_intern("lower_bound")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->lower_bound)); // rbs_node
666667
rb_hash_aset(h, ID2SYM(rb_intern("default_type")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->default_type)); // rbs_node
667668
rb_hash_aset(h, ID2SYM(rb_intern("unchecked")), node->unchecked ? Qtrue : Qfalse);
668669

include/rbs/ast.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ typedef struct rbs_ast_type_param {
450450
struct rbs_ast_symbol *name;
451451
struct rbs_keyword *variance;
452452
struct rbs_node *upper_bound;
453+
struct rbs_node *lower_bound;
453454
struct rbs_node *default_type;
454455
bool unchecked;
455456
} rbs_ast_type_param_t;
@@ -712,7 +713,7 @@ rbs_ast_ruby_annotations_node_type_assertion_t *rbs_ast_ruby_annotations_node_ty
712713
rbs_ast_ruby_annotations_return_type_annotation_t *rbs_ast_ruby_annotations_return_type_annotation_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_location_t *prefix_location, rbs_location_t *return_location, rbs_location_t *colon_location, rbs_node_t *return_type, rbs_location_t *comment_location);
713714
rbs_ast_ruby_annotations_skip_annotation_t *rbs_ast_ruby_annotations_skip_annotation_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_location_t *prefix_location, rbs_location_t *skip_location, rbs_location_t *comment_location);
714715
rbs_ast_string_t *rbs_ast_string_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_string_t string);
715-
rbs_ast_type_param_t *rbs_ast_type_param_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_ast_symbol_t *name, rbs_keyword_t *variance, rbs_node_t *upper_bound, rbs_node_t *default_type, bool unchecked);
716+
rbs_ast_type_param_t *rbs_ast_type_param_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_ast_symbol_t *name, rbs_keyword_t *variance, rbs_node_t *upper_bound, rbs_node_t *lower_bound, rbs_node_t *default_type, bool unchecked);
716717
rbs_method_type_t *rbs_method_type_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_node_list_t *type_params, rbs_node_t *type, rbs_types_block_t *block);
717718
rbs_namespace_t *rbs_namespace_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_node_list_t *path, bool absolute);
718719
rbs_signature_t *rbs_signature_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_node_list_t *directives, rbs_node_list_t *declarations);

include/rbs/lexer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ enum RBSTokenType {
3030
pBANG, /* ! */
3131
pQUESTION, /* ? */
3232
pLT, /* < */
33+
pGT, /* > */
3334
pEQ, /* = */
3435

3536
kALIAS, /* alias */

include/rbs/util/rbs_allocator.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
#define alignof(type) __alignof(type)
1111
#else
1212
// Fallback using offset trick
13-
#define alignof(type) offsetof(struct { char c; type member; }, member)
13+
#define alignof(type) offsetof( \
14+
struct { char c; type member; }, \
15+
member \
16+
)
1417
#endif
1518
#endif
1619

lib/rbs/ast/type_param.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
module RBS
44
module AST
55
class TypeParam
6-
attr_reader :name, :variance, :location, :upper_bound_type, :default_type
6+
attr_reader :name, :variance, :location, :upper_bound_type, :lower_bound_type, :default_type
77

8-
def initialize(name:, variance:, upper_bound:, location:, default_type: nil, unchecked: false)
8+
def initialize(name:, variance:, upper_bound:, lower_bound:, location:, default_type: nil, unchecked: false)
99
@name = name
1010
@variance = variance
1111
@upper_bound_type = upper_bound
12+
@lower_bound_type = lower_bound
1213
@location = location
1314
@default_type = default_type
1415
@unchecked = unchecked
@@ -21,6 +22,13 @@ def upper_bound
2122
end
2223
end
2324

25+
def lower_bound
26+
case lower_bound_type
27+
when Types::ClassInstance, Types::ClassSingleton, Types::Interface
28+
lower_bound_type
29+
end
30+
end
31+
2432
def unchecked!(value = true)
2533
@unchecked = value ? true : false
2634
self
@@ -35,14 +43,15 @@ def ==(other)
3543
other.name == name &&
3644
other.variance == variance &&
3745
other.upper_bound_type == upper_bound_type &&
46+
other.lower_bound_type == lower_bound_type &&
3847
other.default_type == default_type &&
3948
other.unchecked? == unchecked?
4049
end
4150

4251
alias eql? ==
4352

4453
def hash
45-
self.class.hash ^ name.hash ^ variance.hash ^ upper_bound_type.hash ^ unchecked?.hash ^ default_type.hash
54+
self.class.hash ^ name.hash ^ variance.hash ^ upper_bound_type.hash ^ lower_bound_type.hash ^ unchecked?.hash ^ default_type.hash
4655
end
4756

4857
def to_json(state = JSON::State.new)
@@ -52,6 +61,7 @@ def to_json(state = JSON::State.new)
5261
unchecked: unchecked?,
5362
location: location,
5463
upper_bound: upper_bound_type,
64+
lower_bound: lower_bound_type,
5565
default_type: default_type
5666
}.to_json(state)
5767
end
@@ -61,6 +71,10 @@ def map_type(&block)
6171
_upper_bound_type = yield(b)
6272
end
6373

74+
if b = lower_bound_type
75+
_lower_bound_type = yield(b)
76+
end
77+
6478
if dt = default_type
6579
_default_type = yield(dt)
6680
end
@@ -69,6 +83,7 @@ def map_type(&block)
6983
name: name,
7084
variance: variance,
7185
upper_bound: _upper_bound_type,
86+
lower_bound: _lower_bound_type,
7287
location: location,
7388
default_type: _default_type
7489
).unchecked!(unchecked?)
@@ -108,6 +123,7 @@ def self.rename(params, new_names:)
108123
name: new_name,
109124
variance: param.variance,
110125
upper_bound: param.upper_bound_type&.map_type {|type| type.sub(subst) },
126+
lower_bound: param.lower_bound_type&.map_type {|type| type.sub(subst) },
111127
location: param.location,
112128
default_type: param.default_type&.map_type {|type| type.sub(subst) }
113129
).unchecked!(param.unchecked?)
@@ -136,6 +152,10 @@ def to_s
136152
s << " < #{type}"
137153
end
138154

155+
if type = lower_bound_type
156+
s << " > #{type}"
157+
end
158+
139159
if dt = default_type
140160
s << " = #{dt}"
141161
end

lib/rbs/cli/validate.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,13 @@ def validate_class_module_definition
159159
@validator.validate_type(ub, context: nil)
160160
end
161161

162+
if lb = param.lower_bound_type
163+
void_type_context_validator(lb)
164+
no_self_type_validator(lb)
165+
no_classish_type_validator(lb)
166+
@validator.validate_type(lb, context: nil)
167+
end
168+
162169
if dt = param.default_type
163170
void_type_context_validator(dt, true)
164171
no_self_type_validator(dt)
@@ -244,6 +251,13 @@ def validate_interface
244251
@validator.validate_type(ub, context: nil)
245252
end
246253

254+
if lb = param.lower_bound_type
255+
void_type_context_validator(lb)
256+
no_self_type_validator(lb)
257+
no_classish_type_validator(lb)
258+
@validator.validate_type(lb, context: nil)
259+
end
260+
247261
if dt = param.default_type
248262
void_type_context_validator(dt, true)
249263
no_self_type_validator(dt)
@@ -317,6 +331,13 @@ def validate_type_alias
317331
@validator.validate_type(ub, context: nil)
318332
end
319333

334+
if lb = param.lower_bound_type
335+
void_type_context_validator(lb)
336+
no_self_type_validator(lb)
337+
no_classish_type_validator(lb)
338+
@validator.validate_type(lb, context: nil)
339+
end
340+
320341
if dt = param.default_type
321342
void_type_context_validator(dt, true)
322343
no_self_type_validator(dt)

lib/rbs/locator.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ def find_in_type_param(pos, type_param:, array:)
177177
find_in_type(pos, type: upper_bound, array: array) and return true
178178
end
179179

180+
if lower_bound = type_param.lower_bound_type
181+
find_in_type(pos, type: lower_bound, array: array) and return true
182+
end
183+
180184
if default_type = type_param.default_type
181185
find_in_type(pos, type: default_type, array: array) and return true
182186
end

lib/rbs/prototype/rbi.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ def process(node, outer: [], comments:)
235235
variance: variance || :invariant,
236236
location: nil,
237237
upper_bound: nil,
238+
lower_bound: nil,
238239
default_type: nil
239240
)
240241
end
@@ -332,6 +333,7 @@ def method_type(args_node, type_node, variables:, overloads:)
332333
name: name,
333334
variance: :invariant,
334335
upper_bound: nil,
336+
lower_bound: nil,
335337
location: nil,
336338
default_type: nil
337339
)

0 commit comments

Comments
 (0)