Runtime assertions for Ruby literal.fun
ruby
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Prevent subtype recursion loops in recursive deferred graphs

+195 -105
+60 -25
lib/literal.rb
··· 122 122 end 123 123 end 124 124 125 - def self.subtype?(type, supertype) 125 + def self.subtype?(type, supertype, context: nil) 126 + context ||= SubtypeContext.new 127 + raw_key = [type.object_id, supertype.object_id] 128 + raw_acquired = false 129 + resolved_key = nil 130 + resolved_acquired = false 131 + 132 + return context.fetch(raw_key) if context.memoized?(raw_key) 133 + return false unless context.acquire(raw_key) 134 + 135 + raw_acquired = true 136 + 126 137 subtype = type 127 138 128 139 subtype = subtype.block.call if Types::DeferredType === subtype 129 140 supertype = supertype.block.call if Types::DeferredType === supertype 141 + resolved_key = [subtype.object_id, supertype.object_id] 130 142 131 - return true if subtype == Types::NeverType::Instance 143 + if resolved_key != raw_key 144 + if context.memoized?(resolved_key) 145 + result = context.fetch(resolved_key) 146 + context.store(raw_key, result) 147 + return result 148 + end 149 + 150 + return false unless context.acquire(resolved_key) 132 151 133 - return true if supertype == subtype 152 + resolved_acquired = true 153 + end 134 154 135 - case supertype 136 - when Literal::Type 137 - supertype >= subtype 138 - when Module 139 - case subtype 155 + result = if subtype == Types::NeverType::Instance 156 + true 157 + elsif supertype == subtype 158 + true 159 + else 160 + case supertype 161 + when Literal::Type 162 + supertype.>=(subtype, context:) 140 163 when Module 141 - supertype >= subtype 142 - when Numeric 143 - Numeric >= supertype 144 - when String 145 - String >= supertype 146 - when Symbol 147 - Symbol >= supertype 148 - when ::Array 149 - ::Array >= supertype 150 - when ::Hash 151 - ::Hash >= supertype 152 - when Literal::Type 153 - subtype <= supertype 164 + case subtype 165 + when Module 166 + supertype >= subtype 167 + when Numeric 168 + Numeric >= supertype 169 + when String 170 + String >= supertype 171 + when Symbol 172 + Symbol >= supertype 173 + when ::Array 174 + ::Array >= supertype 175 + when ::Hash 176 + ::Hash >= supertype 177 + when Literal::Type 178 + subtype.<=(supertype, context:) 179 + else 180 + false 181 + end 182 + when Range 183 + supertype.cover?(subtype) 154 184 else 155 185 false 156 186 end 157 - when Range 158 - supertype.cover?(subtype) 159 - else 160 - false 187 + end 188 + 189 + context.store(resolved_key, result) 190 + context.store(raw_key, result) 191 + result 192 + ensure 193 + if context 194 + context.release(resolved_key) if resolved_acquired 195 + context.release(raw_key) if raw_acquired 161 196 end 162 197 end 163 198 end
+2 -2
lib/literal/array.rb
··· 20 20 Literal::Array === value && Literal.subtype?(value.__type__, @type) 21 21 end 22 22 23 - def >=(other) 23 + def >=(other, context: nil) 24 24 case other 25 25 when Literal::Array::Generic 26 - Literal.subtype?(other.type, @type) 26 + Literal.subtype?(other.type, @type, context:) 27 27 else 28 28 false 29 29 end
+33
lib/literal/subtype_context.rb
··· 1 + # frozen_string_literal: true 2 + 3 + class Literal::SubtypeContext 4 + def initialize 5 + @memo = {} 6 + @in_progress = {} 7 + end 8 + 9 + attr_reader :memo, :in_progress 10 + 11 + def memoized?(key) 12 + @memo.key?(key) 13 + end 14 + 15 + def fetch(key) 16 + @memo[key] 17 + end 18 + 19 + def store(key, result) 20 + @memo[key] = result 21 + end 22 + 23 + def acquire(key) 24 + return false if @in_progress.key?(key) 25 + 26 + @in_progress[key] = true 27 + true 28 + end 29 + 30 + def release(key) 31 + @in_progress.delete(key) 32 + end 33 + end
+2 -2
lib/literal/tuple.rb
··· 26 26 true 27 27 end 28 28 29 - def >=(other) 29 + def >=(other, context: nil) 30 30 case other 31 31 when Literal::Tuple::Generic 32 32 types = @types ··· 36 36 37 37 i, len = 0, types.size 38 38 while i < len 39 - return false unless Literal.subtype?(other_types[i], types[i]) 39 + return false unless Literal.subtype?(other_types[i], types[i], context:) 40 40 i += 1 41 41 end 42 42
+3 -3
lib/literal/type.rb
··· 1 1 # frozen_string_literal: true 2 2 3 3 module Literal::Type 4 - def >=(other) 4 + def >=(other, context: nil) 5 5 self == other 6 6 end 7 7 8 - def <=(other) 8 + def <=(other, context: nil) 9 9 case other 10 10 when Literal::Type 11 - other >= self 11 + other.>=(self, context:) 12 12 else 13 13 false 14 14 end
+1 -1
lib/literal/types/any_type.rb
··· 14 14 !(nil === value) 15 15 end 16 16 17 - def >=(other) 17 + def >=(other, context: nil) 18 18 !(other === nil) 19 19 end 20 20
+2 -2
lib/literal/types/array_type.rb
··· 24 24 Array === value && value.all?(@type) 25 25 end 26 26 27 - def >=(other) 27 + def >=(other, context: nil) 28 28 case other 29 29 when Literal::Types::ArrayType 30 - Literal.subtype?(other.type, @type) 30 + Literal.subtype?(other.type, @type, context:) 31 31 else 32 32 false 33 33 end
+1 -1
lib/literal/types/boolean_type.rb
··· 14 14 true == value || false == value 15 15 end 16 16 17 - def >=(other) 17 + def >=(other, context: nil) 18 18 case other 19 19 when true, false, Literal::Types::BooleanType 20 20 true
+3 -3
lib/literal/types/class_type.rb
··· 19 19 Class === value && (value == @type || value < @type) 20 20 end 21 21 22 - def >=(other) 22 + def >=(other, context: nil) 23 23 case other 24 24 when Literal::Types::ClassType 25 - Literal.subtype?(other.type, @type) 25 + Literal.subtype?(other.type, @type, context:) 26 26 when Literal::Types::DescendantType 27 - (Class === other.type) && Literal.subtype?(other.type, @type) 27 + (Class === other.type) && Literal.subtype?(other.type, @type, context:) 28 28 else 29 29 false 30 30 end
+7 -7
lib/literal/types/constraint_type.rb
··· 41 41 result 42 42 end 43 43 44 - def >=(other) 44 + def >=(other, context: nil) 45 45 case other 46 46 when Literal::Types::ConstraintType 47 47 other_object_constraints = other.object_constraints 48 48 return false unless @object_constraints.all? do |constraint| 49 - other_object_constraints.any? { |c| Literal.subtype?(c, constraint) } 49 + other_object_constraints.any? { |c| Literal.subtype?(c, constraint, context:) } 50 50 end 51 51 52 52 other_property_constraints = other.property_constraints 53 53 return false unless @property_constraints.all? do |k, v| 54 - Literal.subtype?(other_property_constraints[k], v) 54 + Literal.subtype?(other_property_constraints[k], v, context:) 55 55 end 56 56 57 57 true 58 58 when Literal::Types::IntersectionType 59 59 other_object_constraints = other.types 60 60 return false unless @object_constraints.all? do |constraint| 61 - other_object_constraints.any? { |c| Literal.subtype?(c, constraint) } 61 + other_object_constraints.any? { |c| Literal.subtype?(c, constraint, context:) } 62 62 end 63 63 64 64 true 65 65 when Literal::Types::FrozenType 66 - @object_constraints.all? { |constraint| Literal.subtype?(other.type, constraint) } 66 + @object_constraints.all? { |constraint| Literal.subtype?(other.type, constraint, context:) } 67 67 else 68 68 false 69 69 end 70 70 end 71 71 72 - def <=(other) 72 + def <=(other, context: nil) 73 73 case other 74 74 when Module 75 - @object_constraints.any? { |constraint| Literal.subtype?(constraint, other) } 75 + @object_constraints.any? { |constraint| Literal.subtype?(constraint, other, context:) } 76 76 end 77 77 end 78 78
+2 -2
lib/literal/types/deferred_type.rb
··· 17 17 @block.call === other 18 18 end 19 19 20 - def >=(other) 21 - Literal.subtype?(other, @block.call) 20 + def >=(other, context: nil) 21 + Literal.subtype?(other, @block.call, context:) 22 22 end 23 23 end
+2 -2
lib/literal/types/descendant_type.rb
··· 18 18 Module === value && value < @type 19 19 end 20 20 21 - def >=(other) 21 + def >=(other, context: nil) 22 22 case other 23 23 when Literal::Types::DescendantType, Literal::Types::ClassType 24 - Literal.subtype?(other.type, @type) 24 + Literal.subtype?(other.type, @type, context:) 25 25 else 26 26 false 27 27 end
+2 -2
lib/literal/types/enumerable_type.rb
··· 19 19 Enumerable === value && value.all?(@type) 20 20 end 21 21 22 - def >=(other) 22 + def >=(other, context: nil) 23 23 case other 24 24 when Literal::Types::EnumerableType 25 - Literal.subtype?(other.type, @type) 25 + Literal.subtype?(other.type, @type, context:) 26 26 else 27 27 false 28 28 end
+1 -1
lib/literal/types/falsy_type.rb
··· 18 18 !value 19 19 end 20 20 21 - def >=(other) 21 + def >=(other, context: nil) 22 22 case other 23 23 when Literal::Types::FalsyType, nil, false 24 24 true
+4 -4
lib/literal/types/frozen_type.rb
··· 25 25 value.frozen? && @type === value 26 26 end 27 27 28 - def >=(other) 28 + def >=(other, context: nil) 29 29 case other 30 30 when Literal::Types::FrozenType 31 - Literal.subtype?(other.type, @type) 31 + Literal.subtype?(other.type, @type, context:) 32 32 when Literal::Types::ConstraintType 33 33 type_match = false 34 - frozen_match = Literal.subtype?(other.property_constraints[:frozen?], true) 34 + frozen_match = Literal.subtype?(other.property_constraints[:frozen?], true, context:) 35 35 36 36 other.object_constraints.each do |constraint| 37 37 frozen_match ||= ALWAYS_FROZEN.include?(constraint) 38 - type_match ||= Literal.subtype?(constraint, @type) 38 + type_match ||= Literal.subtype?(constraint, @type, context:) 39 39 return true if frozen_match && type_match 40 40 end 41 41
+3 -3
lib/literal/types/hash_type.rb
··· 31 31 true 32 32 end 33 33 34 - def >=(other) 34 + def >=(other, context: nil) 35 35 case other 36 36 when Literal::Types::HashType 37 37 ( 38 - Literal.subtype?(other.key_type, @key_type) 38 + Literal.subtype?(other.key_type, @key_type, context:) 39 39 ) && ( 40 - Literal.subtype?(other.value_type, @value_type) 40 + Literal.subtype?(other.value_type, @value_type, context:) 41 41 ) 42 42 else 43 43 false
+3 -3
lib/literal/types/interface_type.rb
··· 27 27 true 28 28 end 29 29 30 - def >=(other) 30 + def >=(other, context: nil) 31 31 case other 32 32 when Literal::Types::InterfaceType 33 33 @methods.subset?(other.methods) ··· 35 35 public_methods = other.public_instance_methods.to_set 36 36 @methods.subset?(public_methods) 37 37 when Literal::Types::IntersectionType 38 - other.types.any? { |type| Literal.subtype?(type, self) } 38 + other.types.any? { |type| Literal.subtype?(type, self, context:) } 39 39 when Literal::Types::ConstraintType 40 - other.object_constraints.any? { |type| Literal.subtype?(type, self) } 40 + other.object_constraints.any? { |type| Literal.subtype?(type, self, context:) } 41 41 else 42 42 if OwnClassTypeMethodOwners.include?(other.method(:===).owner) 43 43 self === other
+6 -6
lib/literal/types/intersection_type.rb
··· 31 31 end 32 32 end 33 33 34 - def >=(other) 34 + def >=(other, context: nil) 35 35 case other 36 36 when Literal::Types::IntersectionType 37 37 @types.all? do |type| 38 38 other.types.any? do |other_type| 39 - Literal.subtype?(other_type, type) 39 + Literal.subtype?(other_type, type, context:) 40 40 end 41 41 end 42 42 when Literal::Types::ConstraintType 43 43 @types.all? do |type| 44 44 other.object_constraints.any? do |object_constraint| 45 - Literal.subtype?(object_constraint, type) 45 + Literal.subtype?(object_constraint, type, context:) 46 46 end 47 47 end 48 48 when Literal::Types::FrozenType 49 - @types.all? { |type| Literal.subtype?(other.type, type) } 49 + @types.all? { |type| Literal.subtype?(other.type, type, context:) } 50 50 else 51 51 false 52 52 end 53 53 end 54 54 55 - def <=(other) 55 + def <=(other, context: nil) 56 56 case other 57 57 when Module 58 - @types.any? { |type| Literal.subtype?(type, other) } 58 + @types.any? { |type| Literal.subtype?(type, other, context:) } 59 59 end 60 60 end 61 61
+4 -4
lib/literal/types/json_data_type.rb
··· 50 50 end 51 51 end 52 52 53 - def >=(other) 53 + def >=(other, context: nil) 54 54 return true if COMPATIBLE_TYPES.include?(other) 55 55 56 56 case other 57 57 when Literal::Types::ArrayType 58 - Literal.subtype?(other.type, self) 58 + Literal.subtype?(other.type, self, context:) 59 59 when Literal::Types::HashType 60 - (Literal.subtype?(other.key_type, self) && Literal.subtype?(other.value_type, self)) 60 + (Literal.subtype?(other.key_type, self, context:) && Literal.subtype?(other.value_type, self, context:)) 61 61 when Literal::Types::ConstraintType 62 - other.object_constraints.any? { |type| self >= type } 62 + other.object_constraints.any? { |type| self.>=(type, context:) } 63 63 else 64 64 false 65 65 end
+2 -2
lib/literal/types/map_type.rb
··· 39 39 end 40 40 end 41 41 42 - def >=(other) 42 + def >=(other, context: nil) 43 43 case other 44 44 when Literal::Types::MapType 45 45 other_shape = other.shape 46 46 47 47 @shape.all? do |k, v| 48 - Literal.subtype?(other_shape[k], v) 48 + Literal.subtype?(other_shape[k], v, context:) 49 49 end 50 50 else 51 51 false
+2 -2
lib/literal/types/never_type.rb
··· 14 14 false 15 15 end 16 16 17 - def >=(other) 17 + def >=(other, context: nil) 18 18 case other 19 19 when Literal::Types::NeverType 20 20 true ··· 23 23 end 24 24 end 25 25 26 - def <=(_other) 26 + def <=(_other, context: nil) 27 27 true 28 28 end 29 29
+3 -3
lib/literal/types/nilable_type.rb
··· 23 23 @type.record_literal_type_errors(ctx) if @type.respond_to?(:record_literal_type_errors) 24 24 end 25 25 26 - def >=(other) 26 + def >=(other, context: nil) 27 27 case other 28 28 when Literal::Types::NilableType 29 - Literal.subtype?(other.type, @type) 29 + Literal.subtype?(other.type, @type, context:) 30 30 when nil 31 31 true 32 32 else 33 - Literal.subtype?(other, @type) 33 + Literal.subtype?(other, @type, context:) 34 34 end 35 35 end 36 36
+4 -4
lib/literal/types/not_type.rb
··· 19 19 !(@type === value) 20 20 end 21 21 22 - def >=(other) 22 + def >=(other, context: nil) 23 23 case other 24 24 when Literal::Types::NotType 25 - Literal.subtype?(other.type, @type) 25 + Literal.subtype?(other.type, @type, context:) 26 26 when Literal::Types::ConstraintType 27 - other.object_constraints.any? { |constraint| Literal.subtype?(constraint, self) } 27 + other.object_constraints.any? { |constraint| Literal.subtype?(constraint, self, context:) } 28 28 when Literal::Types::IntersectionType 29 - other.types.any? { |type| Literal.subtype?(type, self) } 29 + other.types.any? { |type| Literal.subtype?(type, self, context:) } 30 30 else 31 31 false 32 32 end
+2 -2
lib/literal/types/range_type.rb
··· 25 25 ) 26 26 end 27 27 28 - def >=(other) 28 + def >=(other, context: nil) 29 29 case other 30 30 when Literal::Types::RangeType 31 - Literal.subtype?(other.type, @type) 31 + Literal.subtype?(other.type, @type, context:) 32 32 else 33 33 false 34 34 end
+2 -2
lib/literal/types/set_type.rb
··· 35 35 end 36 36 end 37 37 38 - def >=(other) 38 + def >=(other, context: nil) 39 39 case other 40 40 when Literal::Types::SetType 41 - Literal.subtype?(other.type, @type) 41 + Literal.subtype?(other.type, @type, context:) 42 42 else 43 43 false 44 44 end
+5 -5
lib/literal/types/tagged_union_type.rb
··· 53 53 raise Literal::ArgumentError.new("No match found for #{value.inspect} in #{inspect}.") 54 54 end 55 55 56 - def >=(other) 56 + def >=(other, context: nil) 57 57 types = @members.values 58 58 59 59 case other 60 60 when Literal::Types::TaggedUnionType 61 - other.members.values.all? { |t| types.any? { |t2| Literal.subtype?(t, t2) } } 61 + other.members.values.all? { |t| types.any? { |t2| Literal.subtype?(t, t2, context:) } } 62 62 when Literal::Types::UnionType 63 - other.types.all? { |t| types.any? { |t2| Literal.subtype?(t, t2) } } && 64 - other.primitives.all? { |p| types.any? { |t| Literal.subtype?(p, t) } } 63 + other.types.all? { |t| types.any? { |t2| Literal.subtype?(t, t2, context:) } } && 64 + other.primitives.all? { |p| types.any? { |t| Literal.subtype?(p, t, context:) } } 65 65 else 66 - types.any? { |t| Literal.subtype?(other, t) } 66 + types.any? { |t| Literal.subtype?(other, t, context:) } 67 67 end 68 68 end 69 69
+1 -1
lib/literal/types/truthy_type.rb
··· 13 13 !!value 14 14 end 15 15 16 - def >=(other) 16 + def >=(other, context: nil) 17 17 case other 18 18 when Literal::Types::TruthyType, true 19 19 true
+1 -1
lib/literal/types/tuple_type.rb
··· 47 47 end 48 48 end 49 49 50 - def >=(other) 50 + def >=(other, context: nil) 51 51 case other 52 52 when Literal::Types::TupleType 53 53 @types == other.types
+8 -8
lib/literal/types/union_type.rb
··· 95 95 Literal::Types::UnionType.new([*@primitives.map(&), *@types.map(&)]) 96 96 end 97 97 98 - def >=(other) 98 + def >=(other, context: nil) 99 99 types = @types 100 100 primitives = @primitives 101 101 102 102 case other 103 103 when Literal::Types::UnionType 104 - other.types.all? { |t| primitives.any? { |p| Literal.subtype?(t, p) } || types.any? { |t2| Literal.subtype?(t, t2) } } && 105 - other.primitives.all? { |p| primitives.any? { |p2| Literal.subtype?(p, p2) } || types.any? { |t| Literal.subtype?(p, t) } } 104 + other.types.all? { |t| primitives.any? { |p| Literal.subtype?(t, p, context:) } || types.any? { |t2| Literal.subtype?(t, t2, context:) } } && 105 + other.primitives.all? { |p| primitives.any? { |p2| Literal.subtype?(p, p2, context:) } || types.any? { |t| Literal.subtype?(p, t, context:) } } 106 106 when Literal::Types::TaggedUnionType 107 - other.members.values.all? { |t| primitives.any? { |p| Literal.subtype?(t, p) } || types.any? { |t2| Literal.subtype?(t, t2) } } 107 + other.members.values.all? { |t| primitives.any? { |p| Literal.subtype?(t, p, context:) } || types.any? { |t2| Literal.subtype?(t, t2, context:) } } 108 108 else 109 - primitives.any? { |p| Literal.subtype?(other, p) } || types.any? { |t| Literal.subtype?(other, t) } 109 + primitives.any? { |p| Literal.subtype?(other, p, context:) } || types.any? { |t| Literal.subtype?(other, t, context:) } 110 110 end 111 111 end 112 112 113 - def <=(other) 113 + def <=(other, context: nil) 114 114 case other 115 115 when Module 116 - @primitives.all? { |primitive| Literal.subtype?(primitive, other) } && 117 - @types.all? { |type| Literal.subtype?(type, other) } 116 + @primitives.all? { |primitive| Literal.subtype?(primitive, other, context:) } && 117 + @types.all? { |type| Literal.subtype?(type, other, context:) } 118 118 end 119 119 end 120 120
+1 -1
lib/literal/types/unit_type.rb
··· 17 17 EQUAL_METHOD.bind_call(@object, value) 18 18 end 19 19 20 - def >=(other) 20 + def >=(other, context: nil) 21 21 case other 22 22 when Literal::Types::UnitType 23 23 EQUAL_METHOD.bind_call(@object, other.object)
+1 -1
lib/literal/types/void_type.rb
··· 14 14 true 15 15 end 16 16 17 - def >=(_) 17 + def >=(_, context: nil) 18 18 true 19 19 end 20 20
+22
test/types/_deferred.test.rb
··· 35 35 assert_subtype Integer, _Deferred { Numeric } 36 36 assert_subtype _Deferred { Integer }, _Deferred { Numeric } 37 37 end 38 + 39 + test "hierarchy with recursive deferred union does not recurse forever" do 40 + recursive = _Union(Integer, _Nilable(_Deferred { recursive })) 41 + 42 + assert_subtype Integer, recursive 43 + refute_subtype Proc, recursive 44 + assert_equal false, Literal.subtype?(Proc, recursive) 45 + end 46 + 47 + test "subtype in-progress guard keeps parent lock" do 48 + recursive = _Union(Integer, _Nilable(_Deferred { recursive })) 49 + 50 + 3.times do 51 + assert_equal false, Literal.subtype?(Proc, recursive) 52 + end 53 + end 54 + 55 + test "subtype handles deferred fresh-wrapper cycles" do 56 + recursive = _Deferred { _Nilable(recursive) } 57 + 58 + assert_equal false, Literal.subtype?(Proc, recursive) 59 + end