Skip to content

Commit ac8348a

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 66c2c91 commit ac8348a

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)