Skip to content

Commit ee9e663

Browse files
authored
Merge pull request #40 from ruby/validate-variance
Validate variance
2 parents d0ac91b + bbcdd35 commit ee9e663

8 files changed

Lines changed: 486 additions & 0 deletions

File tree

lib/ruby/signature.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
require "ruby/signature/builtin_names"
2323
require "ruby/signature/definition"
2424
require "ruby/signature/definition_builder"
25+
require "ruby/signature/variance_calculator"
2526
require "ruby/signature/substitution"
2627
require "ruby/signature/constant"
2728
require "ruby/signature/constant_table"

lib/ruby/signature/ast/declarations.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ def empty?
5353
def size
5454
params.size
5555
end
56+
57+
def rename_to(names)
58+
ModuleTypeParams.new().tap do |params|
59+
names.each.with_index do |new_name, index|
60+
param = self.params[index]
61+
params.add(TypeParam.new(name: new_name, variance: param.variance, skip_validation: param.skip_validation))
62+
end
63+
end
64+
end
5665
end
5766

5867
class Class

lib/ruby/signature/definition_builder.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,8 +506,88 @@ def build_one_instance(type_name, extension_name: nil)
506506
)
507507
end
508508
end
509+
510+
validate_parameter_variance(
511+
decl: decl,
512+
methods: definition.methods
513+
)
514+
end
515+
end
516+
end
517+
518+
def validate_params_with(type_params, result:)
519+
type_params.each do |param|
520+
unless param.skip_validation
521+
unless result.compatible?(param.name, with_annotation: param.variance)
522+
yield param
523+
end
524+
end
525+
end
526+
end
527+
528+
def validate_parameter_variance(decl:, methods:)
529+
type_params = case decl
530+
when AST::Declarations::Extension
531+
env.find_class(decl.name.absolute!).type_params.rename_to(decl.type_params)
532+
else
533+
decl.type_params
534+
end
535+
536+
namespace = decl.name.absolute!.to_namespace
537+
calculator = VarianceCalculator.new(builder: self)
538+
param_names = type_params.each.map(&:name)
539+
540+
errors = []
541+
542+
if decl.is_a?(AST::Declarations::Class)
543+
if decl.super_class
544+
absolute_super_name = absolute_type_name(decl.super_class.name, namespace: namespace, location: decl.location)
545+
absolute_args = decl.super_class.args.map {|type| absolute_type(type, namespace: namespace) }
546+
result = calculator.in_inherit(name: absolute_super_name, args: absolute_args, variables: param_names)
547+
548+
validate_params_with type_params, result: result do |param|
549+
errors.push InvalidVarianceAnnotationError::InheritanceError.new(
550+
param: param
551+
)
552+
end
553+
end
554+
end
555+
556+
decl.members.each do |member|
557+
case member
558+
when AST::Members::Include
559+
if member.name.class?
560+
absolute_module_name = absolute_type_name(member.name, namespace: namespace, location: decl.location)
561+
absolute_args = member.args.map {|type| absolute_type(type, namespace: namespace) }
562+
result = calculator.in_inherit(name: absolute_module_name, args: absolute_args, variables: param_names)
563+
564+
validate_params_with type_params, result: result do |param|
565+
errors.push InvalidVarianceAnnotationError::MixinError.new(
566+
include_member: member,
567+
param: param
568+
)
569+
end
570+
end
509571
end
510572
end
573+
574+
methods.each do |name, method|
575+
method.method_types.each do |method_type|
576+
result = calculator.in_method_type(method_type: method_type, variables: param_names)
577+
578+
validate_params_with type_params, result: result do |param|
579+
errors.push InvalidVarianceAnnotationError::MethodTypeError.new(
580+
method_name: name,
581+
method_type: method_type,
582+
param: param
583+
)
584+
end
585+
end
586+
end
587+
588+
unless errors.empty?
589+
raise InvalidVarianceAnnotationError.new(decl: decl, errors: errors)
590+
end
511591
end
512592

513593
def build_one_singleton(type_name, extension_name: nil)

lib/ruby/signature/errors.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,5 +154,21 @@ def initialize(name, *decls)
154154
super "#{Location.to_string decls.last.location}: Duplicated declaration: #{name}"
155155
end
156156
end
157+
158+
class InvalidVarianceAnnotationError < StandardError
159+
MethodTypeError = Struct.new(:method_name, :method_type, :param, keyword_init: true)
160+
InheritanceError = Struct.new(:super_class, :param, keyword_init: true)
161+
MixinError = Struct.new(:include_member, :param, keyword_init: true)
162+
163+
attr_reader :decl
164+
attr_reader :errors
165+
166+
def initialize(decl:, errors:)
167+
@decl = decl
168+
@errors = errors
169+
170+
super "#{Location.to_string decl.location}: Invalid variance annotation: #{decl.name}"
171+
end
172+
end
157173
end
158174
end

lib/ruby/signature/types.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,20 @@ def each_type
708708
end
709709
end
710710

711+
def each_param(&block)
712+
if block_given?
713+
required_positionals.each(&block)
714+
optional_positionals.each(&block)
715+
rest_positionals&.yield_self(&block)
716+
trailing_positionals.each(&block)
717+
required_keywords.each_value(&block)
718+
optional_keywords.each_value(&block)
719+
rest_keywords&.yield_self(&block)
720+
else
721+
enum_for :each_param
722+
end
723+
end
724+
711725
def to_json(*a)
712726
{
713727
required_positionals: required_positionals,
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
module Ruby
2+
module Signature
3+
class VarianceCalculator
4+
class Result
5+
attr_reader :result
6+
7+
def initialize(variables:)
8+
@result = {}
9+
variables.each do |x|
10+
result[x] = :unused
11+
end
12+
end
13+
14+
def covariant(x)
15+
case result[x]
16+
when :unused
17+
result[x] = :covariant
18+
when :contravariant
19+
result[x] = :invariant
20+
end
21+
end
22+
23+
def contravariant(x)
24+
case result[x]
25+
when :unused
26+
result[x] = :contravariant
27+
when :covariant
28+
result[x] = :invariant
29+
end
30+
end
31+
32+
def invariant(x)
33+
result[x] = :invariant
34+
end
35+
36+
def each(&block)
37+
result.each(&block)
38+
end
39+
40+
def include?(name)
41+
result.key?(name)
42+
end
43+
44+
def compatible?(var, with_annotation:)
45+
variance = result[var]
46+
47+
case
48+
when variance == :unused
49+
true
50+
when with_annotation == :invariant
51+
true
52+
when variance == with_annotation
53+
true
54+
else
55+
false
56+
end
57+
end
58+
end
59+
60+
attr_reader :builder
61+
62+
def initialize(builder:)
63+
@builder = builder
64+
end
65+
66+
def env
67+
builder.env
68+
end
69+
70+
def in_method_type(method_type:, variables:)
71+
result = Result.new(variables: variables)
72+
73+
method_type.type.each_param do |param|
74+
type(param.type, result: result, context: :contravariant)
75+
end
76+
77+
if method_type.block
78+
method_type.block.type.each_param do |param|
79+
type(param.type, result: result, context: :covariant)
80+
end
81+
type(method_type.block.type.return_type, result: result, context: :contravariant)
82+
end
83+
84+
type(method_type.type.return_type, result: result, context: :covariant)
85+
86+
result
87+
end
88+
89+
def in_inherit(name:, args:, variables:)
90+
type = Types::ClassInstance.new(name: name, args: args, location: nil)
91+
92+
Result.new(variables: variables).tap do |result|
93+
type(type, result: result, context: :covariant)
94+
end
95+
end
96+
97+
def type(type, result:, context:)
98+
case type
99+
when Types::Variable
100+
if result.include?(type.name)
101+
case context
102+
when :covariant
103+
result.covariant(type.name)
104+
when :contravariant
105+
result.contravariant(type.name)
106+
when :invariant
107+
result.invariant(type.name)
108+
end
109+
end
110+
when Types::ClassInstance, Types::Interface
111+
decl = env.find_class(type.name)
112+
type.args.each.with_index do |ty, i|
113+
var = decl.type_params.params[i]
114+
case var.variance
115+
when :invariant
116+
type(ty, result: result, context: :invariant)
117+
when :covariant
118+
type(ty, result: result, context: context)
119+
when :contravariant
120+
con = case context
121+
when :invariant
122+
:invariant
123+
when :covariant
124+
:contravariant
125+
when :contravariant
126+
:covariant
127+
end
128+
type(ty, result: result, context: con)
129+
end
130+
end
131+
when Types::Tuple, Types::Record, Types::Union, Types::Intersection
132+
# Covariant types
133+
type.each_type do |ty|
134+
type(ty, result: result, context: context)
135+
end
136+
end
137+
end
138+
end
139+
end
140+
end

0 commit comments

Comments
 (0)