typisch 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +55 -0
- data/lib/typisch/boolean.rb +13 -0
- data/lib/typisch/constructor.rb +112 -0
- data/lib/typisch/datetime.rb +41 -0
- data/lib/typisch/dsl.rb +242 -0
- data/lib/typisch/errors.rb +11 -0
- data/lib/typisch/meta.rb +57 -0
- data/lib/typisch/named_placeholder.rb +67 -0
- data/lib/typisch/null.rb +17 -0
- data/lib/typisch/numeric.rb +80 -0
- data/lib/typisch/object.rb +72 -0
- data/lib/typisch/poset_algorithms.rb +18 -0
- data/lib/typisch/registry.rb +146 -0
- data/lib/typisch/sequence.rb +107 -0
- data/lib/typisch/serialization.rb +80 -0
- data/lib/typisch/string.rb +69 -0
- data/lib/typisch/subtyping.rb +64 -0
- data/lib/typisch/tuple.rb +74 -0
- data/lib/typisch/type.rb +138 -0
- data/lib/typisch/type_checking.rb +12 -0
- data/lib/typisch/typed.rb +133 -0
- data/lib/typisch/union.rb +75 -0
- data/lib/typisch/version.rb +3 -0
- data/lib/typisch.rb +34 -0
- metadata +166 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
# TODO: have this work with whichever of these classes
|
2
|
+
# end up getting required, without having to require them
|
3
|
+
# upfront
|
4
|
+
require 'rational'
|
5
|
+
require 'bigdecimal'
|
6
|
+
require 'complex'
|
7
|
+
|
8
|
+
module Typisch
|
9
|
+
# This is aiming to be a nice numeric tower like those of Scheme etc:
|
10
|
+
# Integral < Rational < Real < Complex
|
11
|
+
#
|
12
|
+
# In these kinds of numeric tower, type and degree of precision is treated as a separate
|
13
|
+
# orthogonal concern; for now I've not treated precision at all here, although
|
14
|
+
# support could be added, eg to allow a distinction between
|
15
|
+
# - fixed precision binary floating point (Float)
|
16
|
+
# - arbitrary precision decimal floating point (BigDecimal)
|
17
|
+
# - fixed size integer (Fixnum)
|
18
|
+
# - arbitrary size integer (Bignum)
|
19
|
+
# - etc
|
20
|
+
# There are quite a few ways to classify numeric types, so I've stuck with just the
|
21
|
+
# most basic mathematical numeric tower classification for now.
|
22
|
+
class Type::Numeric < Type::Constructor
|
23
|
+
|
24
|
+
def initialize(type, *valid_implementation_classes)
|
25
|
+
@type = type
|
26
|
+
@valid_implementation_classes = valid_implementation_classes
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :valid_implementation_classes
|
30
|
+
|
31
|
+
# Note: these are based on how ruby 1.8.7 does it; 1.9 changes
|
32
|
+
# things slightly IIRC so may need tweaks to cope with this.
|
33
|
+
# Either way ruby's hierarchy of numeric types is slightly idiosyncratic:
|
34
|
+
complex = new('Complex', ::Numeric)
|
35
|
+
real = new('Real', ::Precision, ::BigDecimal, ::Rational)
|
36
|
+
rational = new('Rational', ::Rational, ::Integer)
|
37
|
+
integral = new('Integral', ::Integer)
|
38
|
+
|
39
|
+
Registry.register_global_type(:complex, complex)
|
40
|
+
|
41
|
+
Registry.register_global_type(:real, real)
|
42
|
+
Registry.register_global_type(:float, real) # aliasing this as :float too
|
43
|
+
|
44
|
+
Registry.register_global_type(:rational, rational)
|
45
|
+
|
46
|
+
Registry.register_global_type(:integral, integral)
|
47
|
+
Registry.register_global_type(:integer, integral) # aliasing this as :integer too
|
48
|
+
|
49
|
+
TOWER = [complex, real, rational, integral]
|
50
|
+
|
51
|
+
class << self
|
52
|
+
private :new
|
53
|
+
|
54
|
+
def top_type(*)
|
55
|
+
TOWER.first
|
56
|
+
end
|
57
|
+
|
58
|
+
def check_subtype(x, y)
|
59
|
+
x.index_in_tower >= y.index_in_tower
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s(*)
|
64
|
+
@name.inspect
|
65
|
+
end
|
66
|
+
|
67
|
+
def tag
|
68
|
+
@type
|
69
|
+
end
|
70
|
+
|
71
|
+
def index_in_tower
|
72
|
+
TOWER.index {|t| t.equal?(self)}
|
73
|
+
end
|
74
|
+
|
75
|
+
def shallow_check_type(instance)
|
76
|
+
case instance when *@valid_implementation_classes then true else false end
|
77
|
+
end
|
78
|
+
alias :check_type :shallow_check_type
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
class Typisch::Type
|
2
|
+
class Object < Constructor
|
3
|
+
class << self
|
4
|
+
def top_type(*)
|
5
|
+
new("Object")
|
6
|
+
end
|
7
|
+
|
8
|
+
def check_subtype(x, y, &recursively_check_subtype)
|
9
|
+
return false unless x.class_or_module <= y.class_or_module
|
10
|
+
y.property_names_to_types.all? do |y_propname, y_type|
|
11
|
+
x_type = x[y_propname] and recursively_check_subtype[x_type, y_type]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(tag, property_names_to_types={})
|
17
|
+
@tag = tag
|
18
|
+
raise ArgumentError, "expected String tag name for first argument" unless tag.is_a?(::String) && !tag.empty?
|
19
|
+
@property_names_to_types = property_names_to_types
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :tag, :property_names_to_types
|
23
|
+
|
24
|
+
def class_or_module
|
25
|
+
tag.split('::').inject(::Object) {|a,b| a.const_get(b)}
|
26
|
+
end
|
27
|
+
|
28
|
+
def property_names
|
29
|
+
@property_names_to_types.keys
|
30
|
+
end
|
31
|
+
|
32
|
+
def subexpression_types
|
33
|
+
@property_names_to_types.values
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](property_name)
|
37
|
+
@property_names_to_types[property_name]
|
38
|
+
end
|
39
|
+
|
40
|
+
# For now, will only accept classes of object where the properties are available
|
41
|
+
# via attr_reader-style getter methods. TODO: maybe make allowances for objects
|
42
|
+
# which want to type-check via hash-style property access too.
|
43
|
+
def check_type(instance, &recursively_check_type)
|
44
|
+
instance.is_a?(class_or_module) &&
|
45
|
+
@property_names_to_types.all? do |prop_name, type|
|
46
|
+
instance.respond_to?(prop_name) &&
|
47
|
+
recursively_check_type[type, instance.send(prop_name)]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def shallow_check_type(instance)
|
52
|
+
instance.is_a?(class_or_module)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_string(depth, indent)
|
56
|
+
next_indent = "#{indent} "
|
57
|
+
pairs = @property_names_to_types.map {|n,t| "#{n.inspect} => #{t.to_s(depth+1, "#{indent} ")}"}
|
58
|
+
tag = @tag == "Object" ? '' : "#{@tag},"
|
59
|
+
"object(#{tag}\n#{next_indent}#{pairs.join(",\n#{next_indent}")}\n#{indent})"
|
60
|
+
end
|
61
|
+
|
62
|
+
def canonicalize!
|
63
|
+
@property_names_to_types.keys.each do |name|
|
64
|
+
@property_names_to_types[name] = @property_names_to_types[name].target
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def property_annotations(property_name)
|
69
|
+
(annotations[:properties] ||= {})[property_name] ||= {}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Typisch
|
2
|
+
|
3
|
+
class << self
|
4
|
+
# Finds a minimal set of upper bounds amongst the given set of items
|
5
|
+
# from a partially-ordered set, which together cover the whole set.
|
6
|
+
#
|
7
|
+
# In the worst case this will just return the whole set.
|
8
|
+
def find_minimal_set_of_upper_bounds(*items)
|
9
|
+
result = []
|
10
|
+
items.each do |item|
|
11
|
+
next if result.any? {|other| item <= other}
|
12
|
+
result.delete_if {|other| other <= item}
|
13
|
+
result << item
|
14
|
+
end
|
15
|
+
result
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Typisch
|
2
|
+
# A registry is a glorified hash lookup of types by name
|
3
|
+
#
|
4
|
+
# - provide a concise way of referring to more complex types
|
5
|
+
# - help with the wiring up of recursive types
|
6
|
+
# -
|
7
|
+
#
|
8
|
+
class Registry
|
9
|
+
attr_reader :types_by_name, :types_by_class, :types_by_class_and_version
|
10
|
+
|
11
|
+
def initialize(&block)
|
12
|
+
@types_by_name = GLOBALS.dup
|
13
|
+
@pending_canonicalization = {}
|
14
|
+
@types_by_class = {}
|
15
|
+
@types_by_class_and_version = {}
|
16
|
+
register(&block) if block
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](name, version=nil)
|
20
|
+
name = :"#{name}" if name.is_a?(::Module)
|
21
|
+
name = :"#{name}__#{version}" if version
|
22
|
+
@types_by_name[name] ||= Type::NamedPlaceholder.new(name, self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_type(name, type, &callback_on_canonicalization)
|
26
|
+
case @types_by_name[name]
|
27
|
+
when Type::NamedPlaceholder
|
28
|
+
@types_by_name[name].send(:target=, type)
|
29
|
+
when NilClass
|
30
|
+
else
|
31
|
+
raise Error, "type already registered with name #{name.inspect}"
|
32
|
+
end
|
33
|
+
type.send(:name=, name) unless type.name
|
34
|
+
@types_by_name[name] = type
|
35
|
+
@pending_canonicalization[name] = [type, callback_on_canonicalization]
|
36
|
+
end
|
37
|
+
alias :[]= :register_type
|
38
|
+
|
39
|
+
# While loading, we'll register various types in this hash of types
|
40
|
+
# (boolean, string, ...) which we want to be included in all registries
|
41
|
+
GLOBALS = {}
|
42
|
+
def self.register_global_type(name, type)
|
43
|
+
type.send(:name=, name) unless type.name
|
44
|
+
GLOBALS[name] = type
|
45
|
+
end
|
46
|
+
|
47
|
+
# All registering of types in a registry needs to be done inside one of these
|
48
|
+
# blocks; it ensures that the any forward references or cyclic references are
|
49
|
+
# resolved (via canonicalize!-ing every type in the type graph) once you've
|
50
|
+
# finished registering types.
|
51
|
+
#
|
52
|
+
# This also ensures that any uses of recursion are valid / well-founded, and
|
53
|
+
# does any other necessary validation of the type graph you've declared which
|
54
|
+
# isn't possible to do upfront.
|
55
|
+
#
|
56
|
+
# You can nest register blocks without ill-effect; it will only try to
|
57
|
+
# resolve forward references etc once the outermost block has exited.
|
58
|
+
#
|
59
|
+
# Note, this is all very much non-threadsafe, wouldn't be hard to make it so
|
60
|
+
# (probably just slap a big mutex around it) but not sure why exactly you'd
|
61
|
+
# want multi-threaded type registration anyway to anyway so leaving as-is for now.
|
62
|
+
def register(&block)
|
63
|
+
if @registering_types
|
64
|
+
DSLContext.new(self).instance_eval(&block)
|
65
|
+
else
|
66
|
+
start_registering_types!
|
67
|
+
DSLContext.new(self).instance_eval(&block)
|
68
|
+
stop_registering_types!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def start_registering_types!
|
73
|
+
@registering_types = true
|
74
|
+
end
|
75
|
+
|
76
|
+
def stop_registering_types!
|
77
|
+
@registering_types = false
|
78
|
+
|
79
|
+
types = @pending_canonicalization.values.map {|t,c| t}
|
80
|
+
each_type_in_graph(*types) {|t| t.canonicalize!}
|
81
|
+
@pending_canonicalization.each {|name,(type,callback)| callback.call if callback}
|
82
|
+
@pending_canonicalization = {}
|
83
|
+
end
|
84
|
+
|
85
|
+
def each_type_in_graph(*types)
|
86
|
+
seen_so_far = {}
|
87
|
+
while (type = types.pop)
|
88
|
+
next if seen_so_far[type]
|
89
|
+
seen_so_far[type] = true
|
90
|
+
yield type
|
91
|
+
types.push(*type.subexpression_types)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Allow you to dup and merge registries
|
96
|
+
|
97
|
+
def initialize_copy(other)
|
98
|
+
@types_by_name = @types_by_name.dup
|
99
|
+
end
|
100
|
+
|
101
|
+
def merge(other)
|
102
|
+
dup.merge!(other)
|
103
|
+
end
|
104
|
+
|
105
|
+
def merge!(other)
|
106
|
+
@types_by_name.merge!(other.types_by_name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_s
|
110
|
+
pairs = @types_by_name.map do |n,t|
|
111
|
+
next if GLOBALS[n]
|
112
|
+
"r.register #{n.inspect}, #{t.to_s(0, ' ')}"
|
113
|
+
end.compact
|
114
|
+
"Typisch::Registry.new do |r|\n #{pairs.join("\n ")}\nend"
|
115
|
+
end
|
116
|
+
|
117
|
+
def register_type_for_class(klass, type)
|
118
|
+
@types_by_class[klass] = type
|
119
|
+
end
|
120
|
+
|
121
|
+
def register_version_type_for_class(klass, version, type)
|
122
|
+
@types_by_class_and_version[[klass, version]] = type
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# We set up a global registry which you can use if you like, either
|
127
|
+
# via Typisch.global_registry or via the convenience aliases
|
128
|
+
# Typisch.[] and Typisch.register.
|
129
|
+
#
|
130
|
+
# Or, you can make your own registry if you don't want to share a
|
131
|
+
# global registry with other code using this library. (recommended
|
132
|
+
# if writing modular code / library code which uses this).
|
133
|
+
|
134
|
+
def self.global_registry
|
135
|
+
@global_registry ||= Registry.new
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.register(&block)
|
139
|
+
global_registry.register(&block)
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.[](name)
|
143
|
+
global_registry[name]
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class Typisch::Type
|
2
|
+
# A Sequence is an ordered collection of items, all of a given type.
|
3
|
+
#
|
4
|
+
# For now if you want an unordered collection, you just have to treat it
|
5
|
+
# as an ordered collection with arbitrary order; if you want a map/hash you
|
6
|
+
# just treat it as a sequence of tuples. TODO: would be nice to
|
7
|
+
# have more of a hierarchy of collection types here, eg OrderedSequence < Set.
|
8
|
+
#
|
9
|
+
#
|
10
|
+
# (ordered) Sequences support 'slice types', which are a kind of structural
|
11
|
+
# supertype for sequences. Their use is primarily in specifying partial
|
12
|
+
# serializations or partial type-checking for large sequences.
|
13
|
+
#
|
14
|
+
# Eg sequence(:integer, :slice => 0...10)
|
15
|
+
#
|
16
|
+
# This is saying: "A sequence of ints, which may be of any known length, but
|
17
|
+
# where I only care (to validate, serialize, ...) at most the first 10 items".
|
18
|
+
#
|
19
|
+
# Eg sequence(:integer, :slice => 0...10, :total_length => false)
|
20
|
+
#
|
21
|
+
# This is saying: "A sequence of ints, which may be of any known or unknown length,
|
22
|
+
# but where I only care about (validating, serializing, ...) at most the first 10 items,
|
23
|
+
# and I don't care about (validating, serializing...) the total length of the collection
|
24
|
+
class Sequence < Constructor
|
25
|
+
class << self
|
26
|
+
def top_type(overall_top)
|
27
|
+
new(overall_top, :slice => (0...0), :total_length => false)
|
28
|
+
end
|
29
|
+
|
30
|
+
def check_subtype(x, y, &recursively_check_subtype)
|
31
|
+
recursively_check_subtype[x.type, y.type] && (
|
32
|
+
!x.slice ||
|
33
|
+
(y.slice && (
|
34
|
+
x.slice.begin <= y.slice.begin &&
|
35
|
+
x.slice.end >= y.slice.end &&
|
36
|
+
(x.total_length || !y.total_length)
|
37
|
+
))
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(type, options={})
|
43
|
+
@type = type
|
44
|
+
if options[:slice]
|
45
|
+
@slice = options[:slice]
|
46
|
+
@slice = (@slice.begin...@slice.end+1) unless @slice.exclude_end?
|
47
|
+
@total_length = options[:total_length] != false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :slice, :total_length
|
52
|
+
|
53
|
+
def with_options(options)
|
54
|
+
self.class.new(@type, {:slice => @slice, :total_length => @total_length}.merge!(options))
|
55
|
+
end
|
56
|
+
|
57
|
+
def subexpression_types
|
58
|
+
[@type]
|
59
|
+
end
|
60
|
+
|
61
|
+
def check_type(instance, &recursively_check_type)
|
62
|
+
shallow_check_type(instance) && if @slice
|
63
|
+
(instance[@slice] || []).all? {|i| recursively_check_type[@type, i]} &&
|
64
|
+
(!@total_length || ::Integer === instance.length)
|
65
|
+
else
|
66
|
+
instance.all? {|i| recursively_check_type[@type, i]}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# I tried allowing any Enumerable, but this resulted in allowing String and a bunch
|
71
|
+
# of other things which sort of expose a vaguely-array-like interface but not really
|
72
|
+
# in a way that's helpful for typing purposes. E.g. String in 1.8.7 exposes Enumerable
|
73
|
+
# over its *lines*, but an array-like interface over its *characters*, sometimes as
|
74
|
+
# strings, sometimes as ascii char codes. So not consistent at all.
|
75
|
+
#
|
76
|
+
# Any other classes added here must expose Enumerable, but also .length and slices
|
77
|
+
# via [] (at least if you want them to work with slice types).
|
78
|
+
#
|
79
|
+
# For now allowing Hashes too so they can be typed as a sequence of tuples, although should
|
80
|
+
# really only be typed as a set of tuples as there's no ordering or support for slices.
|
81
|
+
VALID_IMPLEMENTATION_CLASSES = [::Array, ::Hash]
|
82
|
+
|
83
|
+
def shallow_check_type(instance)
|
84
|
+
case instance when *VALID_IMPLEMENTATION_CLASSES then true else false end
|
85
|
+
end
|
86
|
+
|
87
|
+
def tag
|
88
|
+
"Sequence"
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :type
|
92
|
+
|
93
|
+
def to_string(depth, indent)
|
94
|
+
result = "sequence(#{@type.to_s(depth+1, indent)}"
|
95
|
+
if @slice
|
96
|
+
result << ", :slice => #{@slice}"
|
97
|
+
result << ", :total_length => false" unless @total_length
|
98
|
+
end
|
99
|
+
result << ")"
|
100
|
+
end
|
101
|
+
|
102
|
+
def canonicalize!
|
103
|
+
@type = @type.target
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Typisch
|
4
|
+
class JSONSerializer
|
5
|
+
def initialize(type, options={})
|
6
|
+
@type = type
|
7
|
+
@options = {}
|
8
|
+
@type_tag_key = (options[:type_tag_key] || '__class__').freeze
|
9
|
+
@class_to_type_tag = options[:class_to_type_tag]
|
10
|
+
@type_tag_to_class = options[:type_tag_to_class] || (@class_to_type_tag && @class_to_type_tag.invert)
|
11
|
+
end
|
12
|
+
|
13
|
+
def class_to_type_tag(klass)
|
14
|
+
@class_to_type_tag ? @class_to_type_tag[klass] : klass.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
def serialize(value)
|
18
|
+
serialize_to_jsonable(value).to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
def serialize_already_encountered_pair(value, type, existing_serialization)
|
22
|
+
raise SerializationError, "cyclic object / type graph when serializing"
|
23
|
+
end
|
24
|
+
|
25
|
+
# http://work.tinou.com/2009/06/the-expression-problem-and-other-mysteries-of-life.html
|
26
|
+
def serialize_to_jsonable(value, type=@type, existing_serializations={})
|
27
|
+
existing = existing_serializations[[type, value]]
|
28
|
+
return serialize_already_encountered_pair(value, type, existing) if existing
|
29
|
+
|
30
|
+
result = case type
|
31
|
+
when Type::Date
|
32
|
+
value.to_s
|
33
|
+
|
34
|
+
when Type::Time
|
35
|
+
value.iso8601
|
36
|
+
|
37
|
+
when Type::Sequence
|
38
|
+
if type.slice
|
39
|
+
slice = value[type.slice]
|
40
|
+
existing_serializations[[type, value]] = result = {
|
41
|
+
@type_tag_key => class_to_type_tag(value.class),
|
42
|
+
'range_start' => type.slice.begin
|
43
|
+
}
|
44
|
+
result['items'] = slice.map {|v| serialize_to_jsonable(v, type.type, existing_serializations)} if slice
|
45
|
+
result['total_items'] = value.length if type.total_length
|
46
|
+
result
|
47
|
+
else
|
48
|
+
result = existing_serializations[[type, value]] = []
|
49
|
+
value.each {|v| result << serialize_to_jsonable(v, type.type, existing_serializations)}
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
when Type::Tuple
|
54
|
+
result = existing_serializations[[type, value]] = []
|
55
|
+
type.types.zip(value).each {|t,v| result << serialize_to_jsonable(v,t,existing_serializations)}
|
56
|
+
result
|
57
|
+
|
58
|
+
when Type::Object
|
59
|
+
result = existing_serializations[[type, value]] = {@type_tag_key => class_to_type_tag(value.class)}
|
60
|
+
type.property_names_to_types.each do |prop_name, type|
|
61
|
+
result[prop_name.to_s] = serialize_to_jsonable(value.send(prop_name), type, existing_serializations)
|
62
|
+
end
|
63
|
+
result
|
64
|
+
|
65
|
+
when Type::Union
|
66
|
+
type = type.alternative_types.find {|t| t.shallow_check_type(value)}
|
67
|
+
raise SerializationError, "No types in union #{type} matched #{value.inspect}, could not serialize" unless type
|
68
|
+
serialize_to_jsonable(value, type, existing_serializations)
|
69
|
+
|
70
|
+
when Type::Constructor # Numeric, Null, String, Boolean etc
|
71
|
+
value
|
72
|
+
|
73
|
+
else
|
74
|
+
raise SerializationError, "Type #{type} not supported for serialization of #{value.inspect}"
|
75
|
+
end
|
76
|
+
|
77
|
+
result
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Typisch
|
2
|
+
# String types support refinement types, specifying a set of allowed values,
|
3
|
+
# or a maximum length.
|
4
|
+
#
|
5
|
+
# About ruby Symbols: these are a pain in the arse.
|
6
|
+
# For now I'm allowing them to type-check interchangably with Strings.
|
7
|
+
# Since Typisch isn't specifically designed for Ruby's quirks but for more general
|
8
|
+
# data interchange, I don't think Symbol should have a special priviledged type
|
9
|
+
# of its own.
|
10
|
+
#
|
11
|
+
# Nevertheless if we ever allow custom type tags on String types (as we do for
|
12
|
+
# Object types at the moment) we could perhaps allow Symbol as a specially-tagged psuedo
|
13
|
+
# string like type. Although it's not a subclass of String, so hmm.
|
14
|
+
class Type::String < Type::Constructor
|
15
|
+
class << self
|
16
|
+
def tag
|
17
|
+
"String"
|
18
|
+
end
|
19
|
+
|
20
|
+
def top_type(*)
|
21
|
+
@top_type ||= new
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_subtype(x, y)
|
25
|
+
x.equal?(y) || (
|
26
|
+
(x.max_length || Infinity) <= (y.max_length || Infinity) &&
|
27
|
+
(!y.values || (x.values && x.values.subset?(y.values)))
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(refinements={})
|
33
|
+
@refinements = refinements
|
34
|
+
if @refinements[:values] && !@refinements[:values].is_a?(::Set)
|
35
|
+
@refinements[:values] = ::Set.new(refinements[:values])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Infinity = 1.0/0
|
40
|
+
|
41
|
+
def max_length
|
42
|
+
@refinements[:max_length]
|
43
|
+
end
|
44
|
+
|
45
|
+
def values
|
46
|
+
@refinements[:values]
|
47
|
+
end
|
48
|
+
|
49
|
+
def tag
|
50
|
+
self.class.tag
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_s(*)
|
54
|
+
@name ? @name.inspect : "string(#{@refinements.inspect})"
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.tag
|
58
|
+
"String"
|
59
|
+
end
|
60
|
+
|
61
|
+
def shallow_check_type(instance)
|
62
|
+
(::String === instance || ::Symbol === instance) &&
|
63
|
+
(!values || values.include?(instance.to_s)) &&
|
64
|
+
(!max_length || instance.to_s.length <= max_length)
|
65
|
+
end
|
66
|
+
|
67
|
+
Registry.register_global_type(:string, top_type)
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Typisch::Type
|
2
|
+
class << self
|
3
|
+
# The core of the subtyping algorithm, which copes with equi-recursive types.
|
4
|
+
#
|
5
|
+
# Actually quite simple on the face of it -- or at least, short.
|
6
|
+
#
|
7
|
+
# The crucial thing is that we allow a goal to be assumed without proof during
|
8
|
+
# the proving of its subgoals. Since (because of the potentially recursive nature
|
9
|
+
# of types) those subgoals may refer to types from the parent goal, we could otherwise
|
10
|
+
# run into infinite loops.
|
11
|
+
#
|
12
|
+
# How's that justified from a logic perspective? well, what we're doing is,
|
13
|
+
# we're just checking that the given subtyping judgement *isn't* provably
|
14
|
+
# *false* under the inference rules at hand. This allows a maximal consistent
|
15
|
+
# set of subtyping judgements to be made.
|
16
|
+
#
|
17
|
+
# Its dual, requiring that the judgement *is* provably *true* under the inference
|
18
|
+
# rules, would only allow a minimal set to be proven, and could get stuck
|
19
|
+
# searching forever for a proof of those judgements which are neither provably false
|
20
|
+
# nor provably true (namely, the awkward recursive ones).
|
21
|
+
#
|
22
|
+
# See Pierce on equi-recursive types and subtyping for the theory:
|
23
|
+
# http://www.cis.upenn.edu/~bcpierce/tapl/, it's an application of
|
24
|
+
# http://en.wikipedia.org/wiki/Knaster–Tarski_theorem to show that this
|
25
|
+
# is a least fixed point with respect to the adding of extra inferences
|
26
|
+
# to a set of subtyping judgements, if you note that those inference rules
|
27
|
+
# are monotonic. 'Corecursion' / 'coinduction' are also terms for what's
|
28
|
+
# going on here.
|
29
|
+
#
|
30
|
+
# TODO: for best performance, should we be going depth-first or breadth-first here?
|
31
|
+
#
|
32
|
+
# Also TODO: when subtype? succeeds (returns true), we can safely save the resulting
|
33
|
+
# set of judgements that were shown to be consistent, for use during future calls to
|
34
|
+
# subtype?. Memoization essentially.
|
35
|
+
def subtype?(x, y, may_assume_proven = {}, depth=0)
|
36
|
+
return true if may_assume_proven[[x,y]]
|
37
|
+
may_assume_proven[[x,y]] = true
|
38
|
+
|
39
|
+
result = check_subtype(x, y) do |u,v|
|
40
|
+
subtype?(u, v, may_assume_proven, depth+1)
|
41
|
+
end
|
42
|
+
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def check_subtype(x, y, &recursively_check_subtype)
|
48
|
+
# Types are either union types, or constructor types. We deal with the unions first.
|
49
|
+
if Union === x
|
50
|
+
x.alternative_types.all? {|t| recursively_check_subtype[t, y]}
|
51
|
+
elsif Union === y
|
52
|
+
y.alternative_types.any? {|t| recursively_check_subtype[x, t]}
|
53
|
+
elsif x.type_lattice == y.type_lattice
|
54
|
+
# Hand over to that specific type_lattice in which both these Type::Constructor types
|
55
|
+
# live, in order to check subtyping goals which are specific to this lattice.
|
56
|
+
x.type_lattice.check_subtype(x, y, &recursively_check_subtype)
|
57
|
+
else
|
58
|
+
# Different Type::Constructor lattices are non-overlapping so we stop unless they're
|
59
|
+
# the same:
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|