Skip to content

Commit 8abac1e

Browse files
committed
Add support for lower bounds in type parameters
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
1 parent a43e90c commit 8abac1e

23 files changed

Lines changed: 1689 additions & 1337 deletions

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: 20 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,8 @@ _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)
768769

769770
_default-type_ ::= (No default type)
770771
| `=` _type_ (The generics parameter has default type)
@@ -834,7 +835,7 @@ class PrettyPrint[T < _Output]
834835
end
835836
```
836837

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.
838+
If a type parameter has an upper bound, the type parameter must be instantiated with types that are a subtype of the upper bound.
838839

839840
```rbs
840841
type str_printer = PrettyPrint[String] # OK
@@ -854,6 +855,21 @@ type foo = _Foo # equivalent to _Foo[untyped]
854855
type bar = _Bar[String] # equivalent to _Bar[String, untyped]
855856
```
856857

858+
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.
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+
857873
Type parameters with default types cannot appear before type parameters without default types. The generic method type parameters cannot have the default types.
858874

859875
### Directives

ext/rbs_extension/ast_translation.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan
700700
rb_hash_aset(h, ID2SYM(rb_intern("name")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->name)); // rbs_ast_symbol
701701
rb_hash_aset(h, ID2SYM(rb_intern("variance")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->variance)); // rbs_keyword
702702
rb_hash_aset(h, ID2SYM(rb_intern("upper_bound")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->upper_bound)); // rbs_node
703+
rb_hash_aset(h, ID2SYM(rb_intern("lower_bound")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->lower_bound)); // rbs_node
703704
rb_hash_aset(h, ID2SYM(rb_intern("default_type")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->default_type)); // rbs_node
704705
rb_hash_aset(h, ID2SYM(rb_intern("unchecked")), node->unchecked ? Qtrue : Qfalse);
705706

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;
@@ -713,7 +714,7 @@ rbs_ast_ruby_annotations_node_type_assertion_t *rbs_ast_ruby_annotations_node_ty
713714
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);
714715
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);
715716
rbs_ast_string_t *rbs_ast_string_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_string_t string);
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 *default_type, bool unchecked);
717+
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);
717718
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);
718719
rbs_namespace_t *rbs_namespace_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_node_list_t *path, bool absolute);
719720
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 */

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
)

lib/rbs/validator.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ def validate_type_params(params, type_name: , method_name: nil, location:)
127127
# @type var each_child: ^(Symbol) { (Symbol) -> void } -> void
128128
each_child = -> (name, &block) do
129129
if param = params.find {|p| p.name == name }
130-
if b = param.upper_bound_type
131-
b.free_variables.each do |tv|
130+
[param.upper_bound_type, param.lower_bound_type].compact.each do |bound|
131+
bound.free_variables.each do |tv|
132132
block[tv]
133133
end
134134
end

0 commit comments

Comments
 (0)