Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ nodes:
c_type: rbs_keyword
- name: upper_bound
c_type: rbs_node
- name: lower_bound
c_type: rbs_node
- name: default_type
c_type: rbs_node
- name: unchecked
Expand Down
37 changes: 33 additions & 4 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ _module-type-parameters_ ::= #

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

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

### Module declaration

Expand All @@ -668,7 +668,7 @@ end

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

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

### Class/module alias declaration

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

_generics-bound_ ::= (No type bound)
| `<` _type_ (The generics parameter is bounded)
| `<` _type_ (The generics parameter has an upper bound)
| '>' _type_ (The generics parameter has a lower bound)

# A type parameter can have both upper and lower bounds, which can be specified in either order:
# `[T < UpperBound > LowerBound]` or `[T > LowerBound < UpperBound]`

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

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

```rbs
type str_printer = PrettyPrint[String] # OK
type int_printer = PrettyPrint[Integer] # Type error
```

If a type parameter has a lower bound, the type parameter must be instantiated with types that are a supertype of the lower bound.

```rbs
class PrettyPrint[T > Numeric]
end

type obj_printer = PrettyPrint[Object] # OK
type int_printer = PrettyPrint[Integer] # Type error
```

A type parameter can have both an upper and a lower bound, and these bounds can be specified in any order.

```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

# This class processes types T that are supertypes of Integer but also subtypes of Numeric.
# This includes Integer, Rational, Complex, Float, and Numeric itself.
def calculate: (T) -> T
end

type int_processor = FlexibleProcessor[Integer] # OK (Integer > Integer and Integer < Numeric)
type num_processor = FlexibleProcessor[Numeric] # OK (Numeric > Integer and Numeric < Numeric)
type obj_processor = FlexibleProcessor[Object] # Type error (Object is not < Numeric)
type str_processor = FlexibleProcessor[String] # Type error (String is not > Integer)
```

The generics type parameter of modules, classes, interfaces, or type aliases can have a default type.

```rbs
Expand Down
1 change: 1 addition & 0 deletions ext/rbs_extension/ast_translation.c
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan
rb_hash_aset(h, ID2SYM(rb_intern("name")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->name)); // rbs_ast_symbol
rb_hash_aset(h, ID2SYM(rb_intern("variance")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->variance)); // rbs_keyword
rb_hash_aset(h, ID2SYM(rb_intern("upper_bound")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->upper_bound)); // rbs_node
rb_hash_aset(h, ID2SYM(rb_intern("lower_bound")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->lower_bound)); // rbs_node
rb_hash_aset(h, ID2SYM(rb_intern("default_type")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->default_type)); // rbs_node
rb_hash_aset(h, ID2SYM(rb_intern("unchecked")), node->unchecked ? Qtrue : Qfalse);

Expand Down
3 changes: 2 additions & 1 deletion include/rbs/ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ typedef struct rbs_ast_type_param {
struct rbs_ast_symbol *name;
struct rbs_keyword *variance;
struct rbs_node *upper_bound;
struct rbs_node *lower_bound;
struct rbs_node *default_type;
bool unchecked;
} rbs_ast_type_param_t;
Expand Down Expand Up @@ -712,7 +713,7 @@ rbs_ast_ruby_annotations_node_type_assertion_t *rbs_ast_ruby_annotations_node_ty
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);
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);
rbs_ast_string_t *rbs_ast_string_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_string_t string);
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);
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);
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);
rbs_namespace_t *rbs_namespace_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_node_list_t *path, bool absolute);
rbs_signature_t *rbs_signature_new(rbs_allocator_t *allocator, rbs_location_t *location, rbs_node_list_t *directives, rbs_node_list_t *declarations);
Expand Down
1 change: 1 addition & 0 deletions include/rbs/lexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum RBSTokenType {
pBANG, /* ! */
pQUESTION, /* ? */
pLT, /* < */
pGT, /* > */
pEQ, /* = */

kALIAS, /* alias */
Expand Down
5 changes: 4 additions & 1 deletion include/rbs/util/rbs_allocator.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
#define alignof(type) __alignof(type)
#else
// Fallback using offset trick
#define alignof(type) offsetof(struct { char c; type member; }, member)
#define alignof(type) offsetof( \
struct { char c; type member; }, \
member \
)
#endif
#endif

Expand Down
26 changes: 23 additions & 3 deletions lib/rbs/ast/type_param.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
module RBS
module AST
class TypeParam
attr_reader :name, :variance, :location, :upper_bound_type, :default_type
attr_reader :name, :variance, :location, :upper_bound_type, :lower_bound_type, :default_type

def initialize(name:, variance:, upper_bound:, location:, default_type: nil, unchecked: false)
def initialize(name:, variance:, upper_bound:, lower_bound:, location:, default_type: nil, unchecked: false)
@name = name
@variance = variance
@upper_bound_type = upper_bound
@lower_bound_type = lower_bound
@location = location
@default_type = default_type
@unchecked = unchecked
Expand All @@ -21,6 +22,13 @@ def upper_bound
end
end

def lower_bound
case lower_bound_type
when Types::ClassInstance, Types::ClassSingleton, Types::Interface
lower_bound_type
end
end

def unchecked!(value = true)
@unchecked = value ? true : false
self
Expand All @@ -35,14 +43,15 @@ def ==(other)
other.name == name &&
other.variance == variance &&
other.upper_bound_type == upper_bound_type &&
other.lower_bound_type == lower_bound_type &&
other.default_type == default_type &&
other.unchecked? == unchecked?
end

alias eql? ==

def hash
self.class.hash ^ name.hash ^ variance.hash ^ upper_bound_type.hash ^ unchecked?.hash ^ default_type.hash
self.class.hash ^ name.hash ^ variance.hash ^ upper_bound_type.hash ^ lower_bound_type.hash ^ unchecked?.hash ^ default_type.hash
end

def to_json(state = JSON::State.new)
Expand All @@ -52,6 +61,7 @@ def to_json(state = JSON::State.new)
unchecked: unchecked?,
location: location,
upper_bound: upper_bound_type,
lower_bound: lower_bound_type,
default_type: default_type
}.to_json(state)
end
Expand All @@ -61,6 +71,10 @@ def map_type(&block)
_upper_bound_type = yield(b)
end

if b = lower_bound_type
_lower_bound_type = yield(b)
end

if dt = default_type
_default_type = yield(dt)
end
Expand All @@ -69,6 +83,7 @@ def map_type(&block)
name: name,
variance: variance,
upper_bound: _upper_bound_type,
lower_bound: _lower_bound_type,
location: location,
default_type: _default_type
).unchecked!(unchecked?)
Expand Down Expand Up @@ -108,6 +123,7 @@ def self.rename(params, new_names:)
name: new_name,
variance: param.variance,
upper_bound: param.upper_bound_type&.map_type {|type| type.sub(subst) },
lower_bound: param.lower_bound_type&.map_type {|type| type.sub(subst) },
location: param.location,
default_type: param.default_type&.map_type {|type| type.sub(subst) }
).unchecked!(param.unchecked?)
Expand Down Expand Up @@ -136,6 +152,10 @@ def to_s
s << " < #{type}"
end

if type = lower_bound_type
s << " > #{type}"
end

if dt = default_type
s << " = #{dt}"
end
Expand Down
21 changes: 21 additions & 0 deletions lib/rbs/cli/validate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ def validate_class_module_definition
@validator.validate_type(ub, context: nil)
end

if lb = param.lower_bound_type
void_type_context_validator(lb)
no_self_type_validator(lb)
no_classish_type_validator(lb)
@validator.validate_type(lb, context: nil)
Comment on lines +163 to +166
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 🤔

end

if dt = param.default_type
void_type_context_validator(dt, true)
no_self_type_validator(dt)
Expand Down Expand Up @@ -244,6 +251,13 @@ def validate_interface
@validator.validate_type(ub, context: nil)
end

if lb = param.lower_bound_type
void_type_context_validator(lb)
no_self_type_validator(lb)
no_classish_type_validator(lb)
@validator.validate_type(lb, context: nil)
end

if dt = param.default_type
void_type_context_validator(dt, true)
no_self_type_validator(dt)
Expand Down Expand Up @@ -317,6 +331,13 @@ def validate_type_alias
@validator.validate_type(ub, context: nil)
end

if lb = param.lower_bound_type
void_type_context_validator(lb)
no_self_type_validator(lb)
no_classish_type_validator(lb)
@validator.validate_type(lb, context: nil)
end

if dt = param.default_type
void_type_context_validator(dt, true)
no_self_type_validator(dt)
Expand Down
4 changes: 4 additions & 0 deletions lib/rbs/locator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ def find_in_type_param(pos, type_param:, array:)
find_in_type(pos, type: upper_bound, array: array) and return true
end

if lower_bound = type_param.lower_bound_type
find_in_type(pos, type: lower_bound, array: array) and return true
end

if default_type = type_param.default_type
find_in_type(pos, type: default_type, array: array) and return true
end
Expand Down
2 changes: 2 additions & 0 deletions lib/rbs/prototype/rbi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ def process(node, outer: [], comments:)
variance: variance || :invariant,
location: nil,
upper_bound: nil,
lower_bound: nil,
default_type: nil
)
end
Expand Down Expand Up @@ -332,6 +333,7 @@ def method_type(args_node, type_node, variables:, overloads:)
name: name,
variance: :invariant,
upper_bound: nil,
lower_bound: nil,
location: nil,
default_type: nil
)
Expand Down
4 changes: 2 additions & 2 deletions lib/rbs/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ def validate_type_params(params, type_name: , method_name: nil, location:)
# @type var each_child: ^(Symbol) { (Symbol) -> void } -> void
each_child = -> (name, &block) do
if param = params.find {|p| p.name == name }
if b = param.upper_bound_type
b.free_variables.each do |tv|
[param.upper_bound_type, param.lower_bound_type].compact.each do |bound|
bound.free_variables.each do |tv|
block[tv]
end
Comment on lines +130 to 133
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.

end
Expand Down
18 changes: 17 additions & 1 deletion schema/typeParam.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,25 @@
}
]
},
"lower_bound": {
"oneOf": [
{
"$ref": "types.json#/definitions/classInstance"
},
{
"$ref": "types.json#/definitions/classSingleton"
},
{
"$ref": "types.json#/definitions/interface"
},
{
"type": "null"
}
]
},
"location": {
"$ref": "location.json"
}
},
"required": ["name", "variance", "unchecked", "upper_bound", "location"]
"required": ["name", "variance", "unchecked", "upper_bound", "lower_bound", "location"]
}
21 changes: 13 additions & 8 deletions sig/type_param.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ module RBS
# Key
# ^^^ name
#
# unchecked out Elem < _ToJson = untyped
# ^^^^^^^^^ unchecked
# ^^^ variance
# ^^^^ name
# ^^^^^^^^^ upper_bound
# ^^^^^^^^^ default
type loc = Location[:name, :variance | :unchecked | :upper_bound | :default]
# unchecked out Elem < _ToJson > bot = untyped
# ^^^^^^^^^ unchecked
# ^^^ variance
# ^^^^ name
# ^^^^^^^^^ upper_bound
# ^^^^^ lower_bound
# ^^^^^^^^ default
type loc = Location[:name, :variance | :unchecked | :upper_bound | :lower_bound | :default]

type variance = :invariant | :covariant | :contravariant

Expand All @@ -24,9 +25,13 @@ module RBS

attr_reader upper_bound_type: Types::t?

%a{pure} def lower_bound: () -> bound?

attr_reader lower_bound_type: Types::t?

attr_reader default_type: Types::t?

def initialize: (name: Symbol, variance: variance, upper_bound: Types::t?, location: loc?, ?default_type: Types::t?, ?unchecked: bool) -> void
def initialize: (name: Symbol, variance: variance, upper_bound: Types::t?, lower_bound: Types::t?, location: loc?, ?default_type: Types::t?, ?unchecked: bool) -> void

include _ToJson

Expand Down
3 changes: 2 additions & 1 deletion src/ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ rbs_ast_string_t *rbs_ast_string_new(rbs_allocator_t *allocator, rbs_location_t
return instance;
}
#line 156 "prism/templates/src/ast.c.erb"
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) {
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) {
rbs_ast_type_param_t *instance = rbs_allocator_alloc(allocator, rbs_ast_type_param_t);

*instance = (rbs_ast_type_param_t) {
Expand All @@ -906,6 +906,7 @@ rbs_ast_type_param_t *rbs_ast_type_param_new(rbs_allocator_t *allocator, rbs_loc
.name = name,
.variance = variance,
.upper_bound = upper_bound,
.lower_bound = lower_bound,
.default_type = default_type,
.unchecked = unchecked,
};
Expand Down
Loading
Loading