rdl 2.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +13 -0
- data/CHANGES.md +35 -0
- data/README.md +153 -116
- data/bin/rdl_query +1 -1
- data/extras/type_tests/typetests.rb +905 -0
- data/lib/rdl.rb +0 -1
- data/lib/rdl/boot.rb +108 -77
- data/lib/rdl/boot_rails.rb +2 -8
- data/lib/rdl/config.rb +44 -17
- data/lib/rdl/contracts/flat.rb +1 -1
- data/lib/rdl/info.rb +3 -3
- data/lib/rdl/query.rb +11 -11
- data/lib/rdl/typecheck.rb +399 -136
- data/lib/rdl/types/finite_hash.rb +3 -2
- data/lib/rdl/types/generic.rb +7 -6
- data/lib/rdl/types/intersection.rb +3 -2
- data/lib/rdl/types/lexer.rex +2 -3
- data/lib/rdl/types/lexer.rex.rb +1 -4
- data/lib/rdl/types/method.rb +7 -6
- data/lib/rdl/types/nominal.rb +10 -1
- data/lib/rdl/types/parser.racc +7 -8
- data/lib/rdl/types/parser.tab.rb +108 -109
- data/lib/rdl/types/structural.rb +1 -0
- data/lib/rdl/types/tuple.rb +2 -2
- data/lib/rdl/types/type.rb +8 -8
- data/lib/rdl/types/type_inferencer.rb +1 -1
- data/lib/rdl/types/union.rb +2 -1
- data/lib/rdl/util.rb +28 -3
- data/lib/rdl/wrap.rb +216 -165
- data/lib/rdl_disable.rb +22 -15
- data/lib/types/core.rb +2 -4
- data/lib/types/core/_aliases.rb +14 -0
- data/lib/types/core/abbrev.rb +3 -0
- data/lib/types/core/array.rb +139 -0
- data/lib/types/core/base64.rb +8 -0
- data/lib/types/core/basic_object.rb +12 -0
- data/lib/types/core/benchmark.rb +9 -0
- data/lib/types/core/bigdecimal.rb +223 -0
- data/lib/types/core/bigmath.rb +10 -0
- data/lib/types/core/bignum.rb +214 -0
- data/lib/types/core/class.rb +15 -0
- data/lib/types/core/complex.rb +123 -0
- data/lib/types/core/coverage.rb +4 -0
- data/lib/types/core/csv.rb +3 -0
- data/lib/types/core/date.rb +4 -0
- data/lib/types/core/dir.rb +37 -0
- data/lib/types/core/encoding.rb +21 -0
- data/lib/types/core/enumerable.rb +96 -0
- data/lib/types/core/enumerator.rb +24 -0
- data/lib/types/core/exception.rb +15 -0
- data/lib/types/core/file.rb +125 -0
- data/lib/types/core/fileutils.rb +4 -0
- data/lib/types/core/fixnum.rb +213 -0
- data/lib/types/core/float.rb +199 -0
- data/lib/types/core/gem.rb +19 -0
- data/lib/types/core/hash.rb +72 -0
- data/lib/types/core/integer.rb +194 -0
- data/lib/types/core/io.rb +101 -0
- data/lib/types/core/kernel.rb +89 -0
- data/lib/types/core/marshal.rb +3 -0
- data/lib/types/core/matchdata.rb +24 -0
- data/lib/types/core/math.rb +50 -0
- data/lib/types/core/module.rb +81 -0
- data/lib/types/core/nil.rb +11 -0
- data/lib/types/core/numeric.rb +56 -0
- data/lib/types/core/object.rb +73 -0
- data/lib/types/core/pathname.rb +104 -0
- data/lib/types/core/proc.rb +12 -0
- data/lib/types/core/process.rb +110 -0
- data/lib/types/core/random.rb +13 -0
- data/lib/types/core/range.rb +37 -0
- data/lib/types/core/rational.rb +207 -0
- data/lib/types/core/regexp.rb +28 -0
- data/lib/types/core/set.rb +56 -0
- data/lib/types/core/string.rb +140 -0
- data/lib/types/core/strscan.rb +6 -0
- data/lib/types/core/symbol.rb +27 -0
- data/lib/types/core/time.rb +66 -0
- data/lib/types/core/uri.rb +18 -0
- data/lib/types/core/yaml.rb +3 -0
- data/lib/types/devise.rb +1 -0
- data/lib/types/devise/controller_helpers.rb +3 -0
- data/lib/types/devise/parameter_sanitizer.rb +2 -0
- data/lib/types/pundit.rb +2 -0
- data/lib/types/rails/_helpers.rb +50 -0
- data/lib/types/rails/abstract_controller/translation.rb +2 -0
- data/lib/types/rails/action_controller/base.rb +3 -0
- data/lib/types/rails/action_controller/instrumentation.rb +6 -0
- data/lib/types/rails/action_controller/metal.rb +3 -0
- data/lib/types/rails/action_controller/mime_responds.rb +13 -0
- data/lib/types/rails/action_controller/parameters.rb +3 -0
- data/lib/types/{rails-5.x → rails}/action_controller/strong_parameters.rb +4 -8
- data/lib/types/rails/action_dispatch/flashhash.rb +8 -0
- data/lib/types/rails/action_dispatch/routing.rb +10 -0
- data/lib/types/rails/action_mailer/base.rb +2 -0
- data/lib/types/rails/action_mailer/message_delivery.rb +2 -0
- data/lib/types/rails/action_view/helpers_sanitizehelper.rb +2 -0
- data/lib/types/rails/action_view/helpers_urlhelper.rb +5 -0
- data/lib/types/rails/active_model/errors.rb +14 -0
- data/lib/types/rails/active_model/validations.rb +2 -0
- data/lib/types/rails/active_record/associations.rb +208 -0
- data/lib/types/rails/active_record/base.rb +2 -0
- data/lib/types/rails/active_record/core.rb +2 -0
- data/lib/types/rails/active_record/finder_methods.rb +2 -0
- data/lib/types/rails/active_record/model_schema.rb +37 -0
- data/lib/types/rails/active_record/relation.rb +11 -0
- data/lib/types/rails/active_record/schema_types.rb +51 -0
- data/lib/types/rails/active_record/validations.rb +2 -0
- data/lib/types/rails/active_support/base.rb +2 -0
- data/lib/types/rails/active_support/logger.rb +3 -0
- data/lib/types/rails/active_support/tagged_logging.rb +2 -0
- data/lib/types/rails/active_support/time_with_zone.rb +13 -0
- data/lib/types/rails/active_support/time_zone.rb +2 -0
- data/lib/types/rails/fixnum.rb +2 -0
- data/lib/types/rails/integer.rb +2 -0
- data/lib/types/rails/rack/request.rb +2 -0
- data/lib/types/rails/string.rb +3 -0
- data/lib/types/rails/time.rb +1 -0
- data/rdl.gemspec +2 -2
- data/test/disabled_test_rdoc.rb +8 -8
- data/test/test_alias.rb +1 -0
- data/test/test_dsl.rb +4 -4
- data/test/test_generic.rb +45 -38
- data/test/test_intersection.rb +10 -10
- data/test/test_le.rb +103 -102
- data/test/test_member.rb +33 -33
- data/test/test_parser.rb +101 -96
- data/test/test_query.rb +84 -84
- data/test/test_rdl.rb +87 -52
- data/test/test_rdl_type.rb +26 -9
- data/test/test_type_contract.rb +32 -31
- data/test/test_typecheck.rb +802 -436
- data/test/test_types.rb +39 -39
- data/test/test_wrap.rb +3 -2
- metadata +91 -120
- data/extras/type_tests/%.rb +0 -171
- data/extras/type_tests/&.rb +0 -159
- data/extras/type_tests/**.rb +0 -222
- data/extras/type_tests/*.rb +0 -177
- data/extras/type_tests/+.rb +0 -170
- data/extras/type_tests/-.rb +0 -171
- data/extras/type_tests/1scomp.rb +0 -157
- data/extras/type_tests/<.rb +0 -170
- data/extras/type_tests/<<.rb +0 -159
- data/extras/type_tests/>>.rb +0 -159
- data/extras/type_tests/[].rb +0 -163
- data/extras/type_tests/^.rb +0 -159
- data/extras/type_tests/abs.rb +0 -155
- data/extras/type_tests/abs2.rb +0 -164
- data/extras/type_tests/angle.rb +0 -157
- data/extras/type_tests/arg.rb +0 -157
- data/extras/type_tests/bit_length.rb +0 -157
- data/extras/type_tests/ceil.rb +0 -157
- data/extras/type_tests/ceilRational.rb +0 -160
- data/extras/type_tests/conj.rb +0 -158
- data/extras/type_tests/defwhere.rb +0 -86
- data/extras/type_tests/denominator.rb +0 -157
- data/extras/type_tests/div.rb +0 -172
- data/extras/type_tests/divslash.rb +0 -179
- data/extras/type_tests/even?.rb +0 -157
- data/extras/type_tests/fdiv.rb +0 -244
- data/extras/type_tests/finite?.rb +0 -157
- data/extras/type_tests/floor.rb +0 -157
- data/extras/type_tests/floorRational.rb +0 -161
- data/extras/type_tests/hash.rb +0 -157
- data/extras/type_tests/imag.rb +0 -158
- data/extras/type_tests/infinite?.rb +0 -157
- data/extras/type_tests/modulo.rb +0 -171
- data/extras/type_tests/nan?.rb +0 -157
- data/extras/type_tests/neg.rb +0 -155
- data/extras/type_tests/next.rb +0 -157
- data/extras/type_tests/next_float.rb +0 -157
- data/extras/type_tests/numerator.rb +0 -157
- data/extras/type_tests/phase.rb +0 -157
- data/extras/type_tests/prev_float.rb +0 -157
- data/extras/type_tests/quo.rb +0 -179
- data/extras/type_tests/rationalize.rb +0 -157
- data/extras/type_tests/rationalizeArg.rb +0 -198
- data/extras/type_tests/real.rb +0 -157
- data/extras/type_tests/real?.rb +0 -157
- data/extras/type_tests/round.rb +0 -157
- data/extras/type_tests/roundArg.rb +0 -169
- data/extras/type_tests/size.rb +0 -157
- data/extras/type_tests/to_c.rb +0 -157
- data/extras/type_tests/to_f.rb +0 -155
- data/extras/type_tests/to_i.rb +0 -157
- data/extras/type_tests/to_r.rb +0 -157
- data/extras/type_tests/to_s.rb +0 -157
- data/extras/type_tests/truncate.rb +0 -157
- data/extras/type_tests/truncateArg.rb +0 -166
- data/extras/type_tests/type tests +0 -1
- data/extras/type_tests/zero?.rb +0 -155
- data/extras/type_tests/|.rb +0 -159
- data/lib/types/core-ruby-2.x/_aliases.rb +0 -15
- data/lib/types/core-ruby-2.x/abbrev.rb +0 -5
- data/lib/types/core-ruby-2.x/array.rb +0 -137
- data/lib/types/core-ruby-2.x/base64.rb +0 -10
- data/lib/types/core-ruby-2.x/basic_object.rb +0 -14
- data/lib/types/core-ruby-2.x/benchmark.rb +0 -11
- data/lib/types/core-ruby-2.x/bigdecimal.rb +0 -224
- data/lib/types/core-ruby-2.x/bigmath.rb +0 -12
- data/lib/types/core-ruby-2.x/bignum.rb +0 -214
- data/lib/types/core-ruby-2.x/class.rb +0 -17
- data/lib/types/core-ruby-2.x/complex.rb +0 -124
- data/lib/types/core-ruby-2.x/coverage.rb +0 -6
- data/lib/types/core-ruby-2.x/csv.rb +0 -5
- data/lib/types/core-ruby-2.x/date.rb +0 -6
- data/lib/types/core-ruby-2.x/dir.rb +0 -38
- data/lib/types/core-ruby-2.x/encoding.rb +0 -23
- data/lib/types/core-ruby-2.x/enumerable.rb +0 -98
- data/lib/types/core-ruby-2.x/enumerator.rb +0 -26
- data/lib/types/core-ruby-2.x/exception.rb +0 -17
- data/lib/types/core-ruby-2.x/file.rb +0 -126
- data/lib/types/core-ruby-2.x/fileutils.rb +0 -6
- data/lib/types/core-ruby-2.x/fixnum.rb +0 -213
- data/lib/types/core-ruby-2.x/float.rb +0 -199
- data/lib/types/core-ruby-2.x/gem.rb +0 -247
- data/lib/types/core-ruby-2.x/hash.rb +0 -72
- data/lib/types/core-ruby-2.x/integer.rb +0 -197
- data/lib/types/core-ruby-2.x/io.rb +0 -103
- data/lib/types/core-ruby-2.x/kernel.rb +0 -90
- data/lib/types/core-ruby-2.x/marshal.rb +0 -5
- data/lib/types/core-ruby-2.x/matchdata.rb +0 -26
- data/lib/types/core-ruby-2.x/math.rb +0 -53
- data/lib/types/core-ruby-2.x/module.rb +0 -83
- data/lib/types/core-ruby-2.x/nil.rb +0 -12
- data/lib/types/core-ruby-2.x/numeric.rb +0 -56
- data/lib/types/core-ruby-2.x/object.rb +0 -75
- data/lib/types/core-ruby-2.x/pathname.rb +0 -106
- data/lib/types/core-ruby-2.x/proc.rb +0 -16
- data/lib/types/core-ruby-2.x/process.rb +0 -127
- data/lib/types/core-ruby-2.x/random.rb +0 -17
- data/lib/types/core-ruby-2.x/range.rb +0 -39
- data/lib/types/core-ruby-2.x/rational.rb +0 -209
- data/lib/types/core-ruby-2.x/regexp.rb +0 -30
- data/lib/types/core-ruby-2.x/set.rb +0 -58
- data/lib/types/core-ruby-2.x/string.rb +0 -143
- data/lib/types/core-ruby-2.x/strscan.rb +0 -7
- data/lib/types/core-ruby-2.x/symbol.rb +0 -29
- data/lib/types/core-ruby-2.x/time.rb +0 -68
- data/lib/types/core-ruby-2.x/uri.rb +0 -20
- data/lib/types/core-ruby-2.x/yaml.rb +0 -5
- data/lib/types/rails-5.x/_helpers.rb +0 -52
- data/lib/types/rails-5.x/action_controller/mime_responds.rb +0 -11
- data/lib/types/rails-5.x/action_dispatch/routing.rb +0 -10
- data/lib/types/rails-5.x/active_model/errors.rb +0 -15
- data/lib/types/rails-5.x/active_model/validations.rb +0 -5
- data/lib/types/rails-5.x/active_record/associations.rb +0 -190
- data/lib/types/rails-5.x/active_record/core.rb +0 -3
- data/lib/types/rails-5.x/active_record/model_schema.rb +0 -39
- data/lib/types/rails-5.x/fixnum.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aabf7af1d05594fdd962964b90285fa322cc9aef
|
4
|
+
data.tar.gz: 435dff6920f147343d71bf459fa8de1462358195
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ae6a8193a9589662d98b35ab30eec6836dcbd123317e7e820916728a8686b0b3e1d914f433d91a44c904468d763eac316bab00886441675246fc46e8ce410e3
|
7
|
+
data.tar.gz: b6503f4c82c45555249c85e08c4ae9d10dc287f62462c22cf36dfaa48d63d6a4151b27b747871024fc4b398ea280bb3584355a08dc0b097f11a0c346f08fc9fb
|
data/.travis.yml
CHANGED
@@ -16,6 +16,19 @@ rvm:
|
|
16
16
|
- 2.2.2
|
17
17
|
- 2.2.3
|
18
18
|
- 2.2.4
|
19
|
+
- 2.2.5
|
20
|
+
- 2.2.6
|
19
21
|
- 2.3.0
|
20
22
|
- 2.3.1
|
23
|
+
- 2.3.2
|
24
|
+
- 2.3.3
|
25
|
+
- 2.3.4
|
26
|
+
- 2.4.0
|
27
|
+
- 2.4.1
|
21
28
|
sudo: false
|
29
|
+
notifications:
|
30
|
+
email:
|
31
|
+
recipients:
|
32
|
+
- rdl-devel@googlegroups.com
|
33
|
+
on_success: never # default: change
|
34
|
+
on_failure: always # default: always
|
data/CHANGES.md
CHANGED
@@ -2,6 +2,41 @@
|
|
2
2
|
|
3
3
|
## [Unreleased]
|
4
4
|
|
5
|
+
## [2.1.0] - 2017-06-14
|
6
|
+
|
7
|
+
### Fixed
|
8
|
+
- Type checking bug in const expressions when env[:self] is SingletonType
|
9
|
+
- Type for unary minus in numeric type files (changed :- to :-@)
|
10
|
+
- Type checking initialize method bugs
|
11
|
+
- Type checking of Module methods
|
12
|
+
- Type checking method when class name is included in type annotation
|
13
|
+
- Dynamic type checks after calls to instantiate!
|
14
|
+
- Ruby 2.4 compatibility!
|
15
|
+
- Various core and standard library types
|
16
|
+
- Parsing bug with `or`
|
17
|
+
- Sub-classes not wrapped bug
|
18
|
+
- Exception from case/when branch with empty body
|
19
|
+
|
20
|
+
### Added
|
21
|
+
- Support operator assignment when left-hand side has method args
|
22
|
+
- Support nested class names
|
23
|
+
- Type for unary plus in numeric type files
|
24
|
+
- Support for instantiate! for binding type parameters during static type checking
|
25
|
+
- New "check" flag for calls to instantiate! indicating whether we want to check type of receiving object on call
|
26
|
+
- More precise static type checking for `Object#class` method
|
27
|
+
- Klass argument to rdl_nowrap, rdl_alias
|
28
|
+
- Some more support for Rails
|
29
|
+
- Support for next/break in block arguments
|
30
|
+
- Support for super in static analysis
|
31
|
+
|
32
|
+
### Changed
|
33
|
+
- Global variables are now module variables of RDL::Globals
|
34
|
+
- at_exit handler only installed if `Config.report` or `.guess_types` are accessed
|
35
|
+
- Subclass Parser::Diagnostic instead of monkey patching it
|
36
|
+
- Replaced `Fixnum` with `Integer` in README (suggested by https://github.com/Dorian)
|
37
|
+
- All annotations removed from `Object` and added to `RDL::Annotate`
|
38
|
+
- `type_cast`, `instantiate!`, and `deinstantiate!` are now part of the `RDL` module to avoid adding them to `Object`
|
39
|
+
|
5
40
|
## [2.0.1] - 2016-11-11
|
6
41
|
|
7
42
|
### Fixed
|
data/README.md
CHANGED
@@ -34,12 +34,13 @@
|
|
34
34
|
* [Type Casts](#type-casts)
|
35
35
|
* [Bottom Type (%bot)](#bottom-type-bot)
|
36
36
|
* [Non-null Type](#non-null-type)
|
37
|
+
* [Constructor Type](#constructor-type)
|
37
38
|
* [Static Type Checking](#static-type-checking)
|
38
39
|
* [Types for Variables](#types-for-variables)
|
39
40
|
* [Tuples, Finite Hashes, and Subtyping](#tuples-finite-hashes-and-subtyping)
|
40
41
|
* [Other Features and Limitations](#other-features-and-limitations)
|
41
42
|
* [Assumptions](#assumptions)
|
42
|
-
* [
|
43
|
+
* [RDL Method Reference](#rdl-method-reference)
|
43
44
|
* [Performance](#performance)
|
44
45
|
* [Queries](#queries)
|
45
46
|
* [Configuration](#configuration)
|
@@ -54,20 +55,22 @@ RDL is a lightweight system for adding types, type checking, and contracts to Ru
|
|
54
55
|
|
55
56
|
```ruby
|
56
57
|
require 'rdl'
|
58
|
+
extend RDL::Annotate # add annotation methods to current scope
|
57
59
|
|
58
|
-
type '(
|
60
|
+
type '(Integer, Integer) -> String'
|
59
61
|
def m(x,y) ... end
|
60
62
|
```
|
61
63
|
|
62
|
-
This indicates that `m` returns a `String` if given two `
|
64
|
+
This indicates that `m` returns a `String` if given two `Integer` arguments. When written as above, RDL enforces this type as a *contract* checked at run time: When `m` is called, RDL checks that `m` is given exactly two arguments and both are `Integers`, and that `m` returns an instance of `String`.
|
63
65
|
|
64
66
|
RDL can also *statically type check* method bodies against their signatures. For example:
|
65
67
|
|
66
68
|
```ruby
|
67
69
|
file.rb:
|
68
70
|
require 'rdl'
|
71
|
+
extend RDL::Annotate
|
69
72
|
|
70
|
-
type '(
|
73
|
+
type '(Integer) -> Integer', typecheck: :now
|
71
74
|
def id(x)
|
72
75
|
"forty-two"
|
73
76
|
end
|
@@ -75,15 +78,22 @@ file.rb:
|
|
75
78
|
|
76
79
|
```
|
77
80
|
$ ruby file.rb
|
78
|
-
.../lib/rdl/typecheck.rb:
|
79
|
-
.../file.rb:
|
80
|
-
.../file.rb:
|
81
|
-
.../file.rb:
|
81
|
+
.../lib/rdl/typecheck.rb:158:in `error': (RDL::Typecheck::StaticTypeError)
|
82
|
+
.../file.rb:6:3: error: got type `String' where return type `Integer' expected
|
83
|
+
.../file.rb:6: "forty-two"
|
84
|
+
.../file.rb:6: ^~~~~~~~~~~
|
82
85
|
```
|
83
86
|
|
84
|
-
Passing `typecheck: :now` to `type` checks the method body immediately or as soon as it is defined. Passing `typecheck: :call` to `type` statically type checks the method body whenever it is called. Passing `typecheck: sym` for some other symbol statically type checks the method body when `
|
87
|
+
Passing `typecheck: :now` to `type` checks the method body immediately or as soon as it is defined. Passing `typecheck: :call` to `type` statically type checks the method body whenever it is called. Passing `typecheck: sym` for some other symbol statically type checks the method body when `RDL.do_typecheck sym` is called.
|
85
88
|
|
86
|
-
|
89
|
+
The `type` method can also be called with the class and method to be annotated, and it can also be invoked as `RDL.type` in case `extend RDL::Annotate` would cause namespace issues:
|
90
|
+
|
91
|
+
```
|
92
|
+
type :A, :id, '(Integer) -> Integer', typecheck: :now # Add a type annotation for A#id.
|
93
|
+
RDL.type :A, :id, '(Integer) -> Integer', typecheck: :now # Note class and method name required when calling like this
|
94
|
+
```
|
95
|
+
|
96
|
+
RDL tries to follow the philosophy that you get what you pay for. Methods with type annotations can be checked dynamically or statically; methods without type annotations are unaffected by RDL. See the [performance](#performance) discussion for more detail.
|
87
97
|
|
88
98
|
RDL supports many more complex type annotations; see below for a complete discussion and examples.
|
89
99
|
|
@@ -93,13 +103,13 @@ RDL types are stored in memory at run time, so it's also possible for programs t
|
|
93
103
|
$ rdl_query String#include? # print type for instance method of another class
|
94
104
|
$ rdl_query Pathname.glob # print type for singleton method of a class
|
95
105
|
$ rdl_query Array # print types for all methods of a class
|
96
|
-
$ rdl_query "(
|
97
|
-
$ rdl_query "(.) ->
|
98
|
-
$ rdl_query "(...,
|
106
|
+
$ rdl_query "(Integer) -> Integer" # print all methods that take an Integer and return an Integer
|
107
|
+
$ rdl_query "(.) -> Integer" # print all methods that take a single arg of any type
|
108
|
+
$ rdl_query "(..., Integer, ...) -> ." # print all methods that take an Integer as some argument
|
99
109
|
|
100
110
|
```
|
101
111
|
|
102
|
-
See below for more details of the query format. The `
|
112
|
+
See below for more details of the query format. The `RDL.query` method performs the same function as long as the gem is loaded, so you can use this in `irb`.
|
103
113
|
|
104
114
|
```ruby
|
105
115
|
$ irb
|
@@ -108,13 +118,14 @@ $ irb
|
|
108
118
|
> require 'types/core'
|
109
119
|
=> true
|
110
120
|
|
111
|
-
>
|
121
|
+
> RDL.query '...' # as above
|
112
122
|
```
|
113
123
|
|
114
124
|
RDL also supports more general contracts, though these can only be enforced at run time and are not statically checked. These more general contracts take the form of *preconditions*, describing what a method assumes about its inputs, and *postconditions*, describing what a method guarantees about its outputs. For example:
|
115
125
|
|
116
126
|
```ruby
|
117
127
|
require 'rdl'
|
128
|
+
extend RDL::Annotate
|
118
129
|
|
119
130
|
pre { |x| x > 0 }
|
120
131
|
post { |r,x| r > 0 }
|
@@ -123,7 +134,7 @@ def sqrt(x)
|
|
123
134
|
end
|
124
135
|
```
|
125
136
|
|
126
|
-
Given this program, RDL intercepts the call to `sqrt` and passes its argument to the `pre` block, which checks that the argument is positive. Then when `sqrt` returns, RDL passes the return value (as `r`) and the initial argument (as `x`) to the `post` block, which checks that the return is positive. (Let's ignore complex numbers to keep things simple...) RDL contracts are enforced at method entry and exit. For example, if we call `sqrt(49)`, RDL first checks that `49 > 0`; then it passes `49` to `sqrt`, which (presumably) returns `7`; then RDL checks that `7 > 0`; and finally it returns `7`. Note that pre- and postconditions can't be searched for using `
|
137
|
+
Given this program, RDL intercepts the call to `sqrt` and passes its argument to the `pre` block, which checks that the argument is positive. Then when `sqrt` returns, RDL passes the return value (as `r`) and the initial argument (as `x`) to the `post` block, which checks that the return is positive. (Let's ignore complex numbers to keep things simple...) RDL contracts are enforced at method entry and exit. For example, if we call `sqrt(49)`, RDL first checks that `49 > 0`; then it passes `49` to `sqrt`, which (presumably) returns `7`; then RDL checks that `7 > 0`; and finally it returns `7`. The `pre` and `post` methods can also be called as `RDL.pre` and `RDL.post`, as long as they are also given class and method arguments, similarly to `type`. Note that pre- and postconditions can't be searched for using `RDL.query`.
|
127
138
|
|
128
139
|
# Using RDL
|
129
140
|
|
@@ -137,12 +148,10 @@ RDL currently supports Ruby 2.x. It may or may not work with other versions.
|
|
137
148
|
|
138
149
|
## Loading RDL
|
139
150
|
|
140
|
-
Use `require 'rdl'` to load the RDL library. If you want to use the core and standard library type signatures that come with RDL, follow it with `require 'types/core'`.
|
151
|
+
Use `require 'rdl'` to load the RDL library. If you want to access the annotation language, add `extend RDL::Annotate` as appropriate. If you want to use the core and standard library type signatures that come with RDL, follow it with `require 'types/core'`. Currently RDL has types for the following versions of Ruby:
|
141
152
|
|
142
153
|
* 2.x
|
143
154
|
|
144
|
-
(Currently all 2.x versions are assumed to have the same library type signatures, which may not be correct.)
|
145
|
-
|
146
155
|
## Disabling RDL
|
147
156
|
|
148
157
|
For performance reasons you probably don't want to use RDL in production code. To disable RDL, replace `require 'rdl'` with `require 'rdl_disable'`. This will cause all invocations of RDL methods to either be no-ops or to do the minimum necessary to preserve the program's semantics (e.g., if the RDL method returns `self`, then so does the `rdl_disable` method.)
|
@@ -165,18 +174,7 @@ to get the head version from github.
|
|
165
174
|
|
166
175
|
In development and test mode, you will now have access to `rdl`, `types/core` from RDL, and extra type annotations for Rails and some related gems. In production mode, RDL will be disabled (by loading `rdl_disable`).
|
167
176
|
|
168
|
-
**Warning:** Rails support is currently extremely limited, not well tested, and generally needs more work...please send bug reports/pull requests/etc and we will fix things.
|
169
|
-
|
170
|
-
Currently, RDL has types for the following versions of Rails:
|
171
|
-
|
172
|
-
* Rails 5.x support - limited to the following:
|
173
|
-
* Models
|
174
|
-
* Generates type annotations for model column getters and setters
|
175
|
-
* `find_by` and `find_by!`
|
176
|
-
* Generates types annotations for methods added by `belongs_to`, `has_one`, `has_many`, `has_and_belongs_to_many`.
|
177
|
-
* Note currently RDL doesn't do very precise type checking of relations
|
178
|
-
* Controllers
|
179
|
-
* `errors`
|
177
|
+
**Warning:** Rails support is currently extremely limited, not well tested, and generally needs more work...please send bug reports/pull requests/etc and we will try to fix things.
|
180
178
|
|
181
179
|
## Preconditions and Postconditions
|
182
180
|
|
@@ -196,6 +194,8 @@ The `post` method can be called in the same ways as `pre`.
|
|
196
194
|
|
197
195
|
Methods can have no contracts, `pre` by itself, `post` by itself, both, or multiple instances of either. If there are multiple contracts, RDL checks that *all* contracts are satisfied, in the order that the contracts were bound to the method.
|
198
196
|
|
197
|
+
Both `pre` and `post` accept an optional named argument `version` that takes a rubygems version requirement string or array of version requirement students. If the current Ruby version does not match the requirement, then the call to `pre` and `post` is ignored.
|
198
|
+
|
199
199
|
## Type Annotations
|
200
200
|
|
201
201
|
The `type` method adds a type contract to a method. It supports the same calling patterns as `pre` and `post`, except rather than a block, it takes a string argument describing the type. More specifically, `type` can be called as:
|
@@ -209,11 +209,13 @@ A type string generally has the form `(typ1, ..., typn) -> typ` indicating a met
|
|
209
209
|
The `type` method can be called with `wrap: false` so the type information is stored but the type is not enforced. For example, due to the way RDL is implemented, the method `String#=~` can't have a type or contract on it because then it won't set the correct `$1` etc variables:
|
210
210
|
|
211
211
|
```ruby
|
212
|
-
type :=~, '(Object) ->
|
212
|
+
type :=~, '(Object) -> Integer or nil', wrap: false # Wrapping this messes up $1 etc
|
213
213
|
```
|
214
214
|
|
215
215
|
For consistency, `pre` and `post` can also be called with `wrap: false`, but this is generally not as useful.
|
216
216
|
|
217
|
+
The `type` method also accepts an optional `version` named argument.
|
218
|
+
|
217
219
|
# RDL Types
|
218
220
|
|
219
221
|
## Nominal Types
|
@@ -221,7 +223,7 @@ For consistency, `pre` and `post` can also be called with `wrap: false`, but thi
|
|
221
223
|
A nominal type is simply a class name, and it matches any object of that class or any subclass.
|
222
224
|
|
223
225
|
```ruby
|
224
|
-
type String, :insert, '(
|
226
|
+
type String, :insert, '(Integer, String) -> String'
|
225
227
|
```
|
226
228
|
|
227
229
|
## Nil Type
|
@@ -253,7 +255,7 @@ Many Ruby methods can take several different types of arguments or return differ
|
|
253
255
|
|
254
256
|
```ruby
|
255
257
|
type IO, :putc, '(Numeric or String) -> %any'
|
256
|
-
type String, :getbyte, '(
|
258
|
+
type String, :getbyte, '(Integer) -> Integer or nil'
|
257
259
|
```
|
258
260
|
|
259
261
|
Note that for `getbyte`, we could leave off the `nil`, but we include it to match the current documentation of this method.
|
@@ -263,10 +265,10 @@ Note that for `getbyte`, we could leave off the `nil`, but we include it to matc
|
|
263
265
|
Sometimes Ruby methods have several different type signatures. (In Java these would be called *overloaded* methods.) In RDL, such methods are assigned a set of type signatures:
|
264
266
|
|
265
267
|
```ruby
|
266
|
-
type String, :[], '(
|
267
|
-
type String, :[], '(
|
268
|
+
type String, :[], '(Integer) -> String or nil'
|
269
|
+
type String, :[], '(Integer, Integer) -> String or nil'
|
268
270
|
type String, :[], '(Range or Regexp) -> String or nil'
|
269
|
-
type String, :[], '(Regexp,
|
271
|
+
type String, :[], '(Regexp, Integer) -> String or nil'
|
270
272
|
type String, :[], '(Regexp, String) -> String or nil'
|
271
273
|
type String, :[], '(String) -> String or nil'
|
272
274
|
```
|
@@ -322,7 +324,7 @@ type String, :delete, '(String, *String) -> String'
|
|
322
324
|
RDL allows arguments to be named, for documentation purposes. Names are given after the argument's type, and they do not affect type contract checking in any way. For example:
|
323
325
|
|
324
326
|
```ruby
|
325
|
-
type
|
327
|
+
type Integer, :to_s, '(?Integer base) -> String'
|
326
328
|
```
|
327
329
|
|
328
330
|
Here we've named the first argument of `to_s` as `base` to give some extra hint as to its meaning.
|
@@ -343,7 +345,7 @@ Here, RDL will check that the `sqrt` method is called on an argument of type `Fl
|
|
343
345
|
Dependencies can also exist across a method's arguments and return value:
|
344
346
|
|
345
347
|
```ruby
|
346
|
-
type '(
|
348
|
+
type '(Integer x {{ x>y }}, Integer y) -> Float z {{ z==(x+y) }}'
|
347
349
|
def m(x,y) ... end
|
348
350
|
```
|
349
351
|
|
@@ -352,7 +354,7 @@ Any arbitrary code can be placed between the double braces of a type refinement,
|
|
352
354
|
Most pre- and postconditions can be translated into a dependent type by attaching the precondition to one of the arguments and the postcondition to the return. Note, however, that dependently typed positions must always have a name, even if the associated refinment doesn't refer to that name:
|
353
355
|
|
354
356
|
```ruby
|
355
|
-
type '(
|
357
|
+
type '(Integer x {{ $y > 0 }}) -> nil' # argument name must be present even though refinment doesn't use it.
|
356
358
|
```
|
357
359
|
|
358
360
|
## Higher-order Types
|
@@ -360,14 +362,14 @@ type '(Fixnum x {{ $y > 0 }}) -> nil' # argument name must be present even th
|
|
360
362
|
RDL supports types for arguments or return values which are themselves `Proc` objects. Simply enclose the corresponding argument's type with braces to denote that it is a `Proc`. For example:
|
361
363
|
|
362
364
|
```ruby
|
363
|
-
type '(
|
365
|
+
type '(Integer, {(Integer) -> Integer}) -> Integer'
|
364
366
|
def m(x, y) ... end
|
365
367
|
```
|
366
368
|
|
367
|
-
The type annotation above states that the method m takes two arguments: one of type `
|
369
|
+
The type annotation above states that the method m takes two arguments: one of type `Integer`, and another which is a `Proc` which itself takes an `Integer` and returns an `Integer`. A `Proc` may be the return value of a method as well:
|
368
370
|
|
369
371
|
```ruby
|
370
|
-
type '(
|
372
|
+
type '(Integer) -> {(Float) -> Float}'
|
371
373
|
def m(x) ... end
|
372
374
|
```
|
373
375
|
|
@@ -376,18 +378,18 @@ These higher-order types are checked by wrapping the corresponding `Proc` argume
|
|
376
378
|
A type contract can be provided for a method block as well. The block's type should be included after the method argument types:
|
377
379
|
|
378
380
|
```ruby
|
379
|
-
type '(
|
381
|
+
type '(Integer, Float) {(Integer, String) -> String } -> Float'
|
380
382
|
def m(x,y,&blk) ... end
|
381
383
|
```
|
382
384
|
|
383
385
|
Note that this notation will work whether or not a method block is explicitly referenced in the parameters, i.e., whether or not `&blk` is included above. Finally, dependent types work across higher order contracts:
|
384
386
|
|
385
387
|
```ruby
|
386
|
-
type '(
|
388
|
+
type '(Integer x, Float y) -> {(Integer z {{ z>y }}) -> Integer}'
|
387
389
|
def m(x,y,&blk) ... end
|
388
390
|
```
|
389
391
|
|
390
|
-
The type contract above states that method `m` returns a `Proc` which takes
|
392
|
+
The type contract above states that method `m` returns a `Proc` which takes an `Integer z` which must be greater than the argument `Float y`. Whenever this `Proc` is called, it will be checked that this contract holds.
|
391
393
|
|
392
394
|
## Class/Singleton Method Types
|
393
395
|
|
@@ -399,12 +401,6 @@ type File, 'self.dirname', '(String file) -> String dir'
|
|
399
401
|
|
400
402
|
(Notice also the use of a named return type, which we haven't seen before.)
|
401
403
|
|
402
|
-
Type signatures can be added to `initialize` by giving a type signature for `self.new`:
|
403
|
-
|
404
|
-
```ruby
|
405
|
-
type File, 'self.new', '(String file, ?String mode, ?String perm, ?Fixnum opt) -> File'
|
406
|
-
```
|
407
|
-
|
408
404
|
## Structural Types
|
409
405
|
|
410
406
|
Some Ruby methods are intended to take any object that has certain methods. RDL uses *structural types* to denote such cases:
|
@@ -422,7 +418,7 @@ The actual checking that RDL does here varies depending on what type information
|
|
422
418
|
Not to be confused with types for singleton methods, RDL includes *singleton types* that denote positions that always have one particular value; this typically happens only in return positions. For example, `Dir#mkdir` always returns the value 0:
|
423
419
|
|
424
420
|
```ruby
|
425
|
-
type Dir, 'self.mkdir', '(String, ?
|
421
|
+
type Dir, 'self.mkdir', '(String, ?Integer) -> 0'
|
426
422
|
```
|
427
423
|
|
428
424
|
In RDL, any integer or floating point number denotes a singleton type. Arbitrary values can be turned into singleton types by wrapping them in `${.}`. For example, `Float#angle` always returns 0 or pi.
|
@@ -478,7 +474,7 @@ type String, :==, '(%any) -> %bool'
|
|
478
474
|
|
479
475
|
Note it is not a bug that `==` is typed to allow any object. Though you would think that developers would generally only compare objects of the same class (since otherwise `==` almost always returns false), in practice a lot of code does compare objects of different classes.
|
480
476
|
|
481
|
-
Method `type_alias(name, typ)` can be used to create a user-defined type alias, where `name` must begin with `%`:
|
477
|
+
Method `type_alias(name, typ)` (part of `RDL::Annotate`) can be used to create a user-defined type alias, where `name` must begin with `%`:
|
482
478
|
|
483
479
|
```ruby
|
484
480
|
type_alias '%real', 'Integer or Float or Rational'
|
@@ -490,7 +486,7 @@ Type aliases have to be created before they are used (so above, `%path` must be
|
|
490
486
|
|
491
487
|
## Generic Class Types
|
492
488
|
|
493
|
-
RDL supports *parametric polymorphism* for classes, a.k.a. *generics*. The `type_params` method names the type parameters of the class, and those parameters can then be used inside type signatures:
|
489
|
+
RDL supports *parametric polymorphism* for classes, a.k.a. *generics*. The `type_params` method (part of `RDL::Annotate`) names the type parameters of the class, and those parameters can then be used inside type signatures:
|
494
490
|
|
495
491
|
```ruby
|
496
492
|
class Array
|
@@ -500,27 +496,27 @@ class Array
|
|
500
496
|
end
|
501
497
|
```
|
502
498
|
|
503
|
-
Here the first argument to `type_params` is a list of symbols or strings that name the type parameters. In this case there is one parameter, `t`, and it is the return type of `shift`.
|
499
|
+
Here the first argument to `type_params` is a list of symbols or strings that name the type parameters. In this case there is one parameter, `t`, and it is the return type of `shift`. The `type_params` method accepts an optional first argument, the class whose type parameters to set (this defaults to `self`).
|
504
500
|
|
505
|
-
Generic types are applied to type arguments using `<...>` notation, e.g., `Array<
|
501
|
+
Generic types are applied to type arguments using `<...>` notation, e.g., `Array<Integer>` is an `Array` class where `t` is replaced by `Integer`. Thus, for example, if `o` is an `Array<Integer>`, then `o.shift` returns `Integer`. As another example, here is the type for the `[]` method of `Array`:
|
506
502
|
|
507
503
|
```ruby
|
508
504
|
type Array, :[], '(Range) -> Array<t>'
|
509
|
-
type Array, :[], '(
|
510
|
-
type Array, :[], '(
|
505
|
+
type Array, :[], '(Integer or Float) -> t'
|
506
|
+
type Array, :[], '(Integer, Integer) -> Array<t>'
|
511
507
|
```
|
512
508
|
|
513
|
-
Thus if `o` is again an `Array<
|
509
|
+
Thus if `o` is again an `Array<Integer>`, then `o[0]` returns an `Integer` and `o[0..5]` returns an `Array<Integer>`.
|
514
510
|
|
515
|
-
In general it's impossible to assign generic types to objects without knowing the programmer's intention. For example, consider code as simple as `x = [1,2]`. Is it the programmer's intention that `x` is an `Array<
|
511
|
+
In general it's impossible to assign generic types to objects without knowing the programmer's intention. For example, consider code as simple as `x = [1,2]`. Is it the programmer's intention that `x` is an `Array<Integer>`? `Array<Numeric>`? `Array<Object>`?
|
516
512
|
|
517
513
|
Thus, by default, even though `Array` is declared to take type parameters, by default RDL treats array objects at the *raw* type `Array`, which means the type parameters are ignored whenever they appear in types. For our example, this means a call such as `x.push("three")` would not be reported as an error (the type signature of `Array#push` is `'(?t) -> Array<t>'`).
|
518
514
|
|
519
|
-
To fully enforce generic types, RDL requires that the developer `instantiate!` an object with the desired type parameters:
|
515
|
+
To fully enforce generic types, RDL requires that the developer `RDL.instantiate!` an object with the desired type parameters:
|
520
516
|
|
521
517
|
```ruby
|
522
518
|
x = [1,2]
|
523
|
-
|
519
|
+
RDL.instantiate!(x, 'Integer')
|
524
520
|
x.push("three") # type error
|
525
521
|
```
|
526
522
|
|
@@ -530,11 +526,11 @@ y = x
|
|
530
526
|
y.push("three") # also a type error
|
531
527
|
```
|
532
528
|
|
533
|
-
|
529
|
+
Calls to `RDL.instantiate!` may also come with a `check` flag. By default, `check` is set to false. When `check` is set to true, we ensure that the receiving object's contents are consistent with the given type *at the time of the call to* `RDL.instantiate!`. Currently this is enforced using the second parameter to `type_params`, which must name a method that behaves like `Array#all?`, i.e., it iterates through the contents, checking that a block argument is satisfied. As seen above, for `Array` we call `type_params(:t, :all?)`. Then at the call `x.instantiate('Integer', check: true)`, RDL will call `Array#all?` to iterate through the contents of `x` to check they have type `Integer`. A simple call to `RDL.instantiate!(x, 'Integer')`, on the other hand, will not check the types of the elements of `x`. The `check` flag thus leaves to the programmer this choice between dynamic type safety and performance.
|
534
530
|
|
535
|
-
RDL also includes a `deinstantiate!` method to remove the type instantiation from an object:
|
531
|
+
RDL also includes a `RDL.deinstantiate!` method to remove the type instantiation from an object:
|
536
532
|
```ruby
|
537
|
-
|
533
|
+
RDL.deinstantiate!(x)
|
538
534
|
x.push("three") # no longer a type error
|
539
535
|
```
|
540
536
|
|
@@ -549,31 +545,33 @@ The rules for variances are standard. Let's assume `A` is a subclass of `B`. Als
|
|
549
545
|
|
550
546
|
## Tuple Types
|
551
547
|
|
552
|
-
A type such as `Array<
|
548
|
+
A type such as `Array<Integer>` is useful for homogeneous arrays, where all elements have the same type. But Ruby programs often use heterogeneous arrays, e.g., `[1, "two"]`. The best generic type we can give this is `Array<Integer or String>`, but that's imprecise.
|
553
549
|
|
554
|
-
RDL includes special *tuple types* to handle this situation. Tuple types are written `[t1, ..., tn]`, denoting an `Array` of `n` elements of types `t1` through `tn`, in that order. For example, `[1, "two"]` has type `[
|
550
|
+
RDL includes special *tuple types* to handle this situation. Tuple types are written `[t1, ..., tn]`, denoting an `Array` of `n` elements of types `t1` through `tn`, in that order. For example, `[1, "two"]` has type `[Integer, String]`. As another example, here is the type of `Process#getrlimit`, which returns a two-element array of `Integers`:
|
555
551
|
|
556
552
|
```ruby
|
557
|
-
type Process, 'self.getrlimit', '(Symbol or String or
|
553
|
+
type Process, 'self.getrlimit', '(Symbol or String or Integer resource) -> [Integer, Integer] cur_max_limit'
|
558
554
|
```
|
559
555
|
|
560
556
|
## Finite Hash Types
|
561
557
|
|
562
558
|
Similarly to tuple types, RDL also supports *finite hash types* for heterogeneous hashes. Finite hash types are written `{k1 => v1, ..., kn => vn}` to indicate a `Hash` with `n` mappings of type `ki` maps to `vi`. The `ki` may be strings, integers, floats, or constants denoted with `${.}`. If a key is a symbol, then the mapping should be written `ki: vi`. In the latter case, the `{}`'s can be left off:
|
563
559
|
```ruby
|
564
|
-
type MyClass, :foo, '(a:
|
560
|
+
type MyClass, :foo, '(a: Integer, b: String) { () -> %any } -> %any'
|
565
561
|
```
|
566
|
-
Here `foo`, takes a hash where key `:a` is mapped to
|
562
|
+
Here `foo`, takes a hash where key `:a` is mapped to an `Integer` and key `:b` is mapped to a `String`. Similarly, `{'a'=>Integer, 2=>String}` types a hash where keys `'a'` and `2` are mapped to `Integer` and `String`, respectively. Both syntaxes can be used to define hash types.
|
567
563
|
|
568
564
|
RDL also allows a "rest" type in finite hashes (of course, they're not so finite if they use it!):
|
569
565
|
```ruby
|
570
|
-
type MyClass, :foo, '(a:
|
566
|
+
type MyClass, :foo, '(a: Integer, b: String, **Float) -> %any'
|
571
567
|
```
|
572
|
-
In this method, `a` is
|
568
|
+
In this method, `a` is an `Integer`, `b` is a `String`, and any number (zero or more) remaining keyword arguments can be passed where the values have type `Float`, e.g., a call `foo(a: 3, b: 'b', pi: 3.14)` is allowed.
|
573
569
|
|
574
570
|
## Type Casts
|
575
571
|
|
576
|
-
Sometimes RDL does not have precise information about an object's type (this is most useful during static type checking). For these cases, RDL supports type casts of the form `
|
572
|
+
Sometimes RDL does not have precise information about an object's type (this is most useful during static type checking). For these cases, RDL supports type casts of the form `RDL.type_cast(o, t)`. This call returns a new object that delegates all methods to `o` but that will be treated by RDL as if it had type `t`. If `force: true` is passed to `RDL.type_cast`, RDL will perform the cast without checking whether `o` is actually a member of the given type. For example, `x = RDL.type_cast('a', 'nil', force: true)` will make RDL treat `x` as if it had type `nil`, even though it's a `String`.
|
573
|
+
|
574
|
+
Similarly, if an object's type is parameterized (see [Generic Types](#generic-class-types) above), RDL might not statically know the correct instantiation of the object's type parameters. In this case, we can use `RDL.instantiate!` to provide the proper type parameter bindings. For instance, if `a` has type `Array`, but we want RDL to know that `a`'s elements are all `Integer`s or `String`s, we can call `RDL.instantiate!(a, 'Integer or String')`.
|
577
575
|
|
578
576
|
## Bottom Type (%bot)
|
579
577
|
|
@@ -583,15 +581,23 @@ RDL also includes a special *bottom* type `%bot` that is a subtype of any type,
|
|
583
581
|
|
584
582
|
Types can be prefixed with `!` to indicate the associated value is not `nil`. For example:
|
585
583
|
|
586
|
-
`type :x=, '(!
|
584
|
+
`type :x=, '(!Integer) -> !Integer' # x's argument must not be nil`
|
587
585
|
|
588
586
|
**Warning:** This is simply *documentation* of non-nullness, and **is not checked** by the static type checker. The contract checker might or might not enforce non-nullness. (For those who are curious: RDL has this annotation because it seems useful for descriptive purposes. However, it's quite challenging to build a practical analysis that enforces non-nilness without reporting too many false positives.)
|
589
587
|
|
588
|
+
## Constructor Type
|
589
|
+
|
590
|
+
Type signatures can be added to constructors by giving a type signature for `initialize` (not for `new` or `self.new`). The return type for `initialize` must always be `self` or a [generic type](#generic-class-types) where the base is `self`:
|
591
|
+
|
592
|
+
```ruby
|
593
|
+
type :initialize, '(String, Fixnum) -> self'
|
594
|
+
```
|
595
|
+
|
590
596
|
# Static Type Checking
|
591
597
|
|
592
598
|
As mentioned in the introduction, calling `type` with `typecheck: :now` statically type checks the body of the annotated method body against the given signature. If the method has already been defined, RDL will try to check the method immediately. Otherwise, RDL will statically type check the method as soon as it is loaded.
|
593
599
|
|
594
|
-
Often method bodies cannot be type checked as soon as they are loaded because they refer to classes, methods, and variables that have not been created yet. To support these cases, some other symbol can be supplied as `typecheck: sym`. Then when `
|
600
|
+
Often method bodies cannot be type checked as soon as they are loaded because they refer to classes, methods, and variables that have not been created yet. To support these cases, some other symbol can be supplied as `typecheck: sym`. Then when `RDL.do_typecheck sym` is called, all methods typechecked at `sym` will be statically checked.
|
595
601
|
|
596
602
|
Additionally, `type` can be called with `typecheck: :call`, which will delay checking the method's type until the method is called. Currently these checks are not cached, so expect a big performance hit for using this feature.
|
597
603
|
|
@@ -600,11 +606,11 @@ To perform type checking, RDL needs source code, which it gets by parsing the fi
|
|
600
606
|
```ruby
|
601
607
|
[2] pry(main)> require 'rdl'
|
602
608
|
[3] pry(main)> require 'types/core'
|
603
|
-
[4] pry(main)> type '() ->
|
609
|
+
[4] pry(main)> type '() -> Integer', typecheck: :later # note: typecheck: :now doesn't work in pry
|
604
610
|
[5] pry(main)> def f; 'haha'; end
|
605
|
-
[6] pry(main)>
|
611
|
+
[6] pry(main)> RDL.do_typecheck :later
|
606
612
|
RDL::Typecheck::StaticTypeError:
|
607
|
-
(string):2:3: error: got type `String' where return type `
|
613
|
+
(string):2:3: error: got type `String' where return type `Integer' expected
|
608
614
|
(string):2: 'haha'
|
609
615
|
(string):2: ^~~~~~
|
610
616
|
from .../typecheck.rb:158:in `error'
|
@@ -619,27 +625,27 @@ Next we discuss some special features of RDL's type system and some of its limit
|
|
619
625
|
In a standard type system, local variables have one type throughout a method or function body. For example, in C and Java, declaring `int x` means `x` can only be used as an integer. However, in Ruby, variables need not be declared before they are used. Thus, by default, RDL treats local variables *flow-sensitively*, meaning at each assignment to a local variable, the variable's type is replaced by the type of the right hand side. For example:
|
620
626
|
|
621
627
|
```ruby
|
622
|
-
x = 3 # Here `x` is a `
|
628
|
+
x = 3 # Here `x` is a `Integer`
|
623
629
|
x = "three" # Now `x` is a `String`
|
624
630
|
```
|
625
|
-
(Note this is a slight fib, since after the first line, `x` will actually have the singleton type `3`. But we'll ignore this just to keep the discussion a bit simpler, especially since `3` is a subtype of `
|
631
|
+
(Note this is a slight fib, since after the first line, `x` will actually have the singleton type `3`. But we'll ignore this just to keep the discussion a bit simpler, especially since `3` is a subtype of `Integer`.)
|
626
632
|
|
627
633
|
After conditionals, variables have the union of the types they have along both branches:
|
628
634
|
|
629
635
|
```ruby
|
630
636
|
if (some condition) then x = 3 else x = "three" end
|
631
|
-
# x has type `
|
637
|
+
# x has type `Integer or String`
|
632
638
|
```
|
633
639
|
|
634
|
-
RDL also provides a method `var_type` that can be used to force a local variable to have a single type through a method body, i.e., to treat it *flow-insensitively* like a standard type system:
|
640
|
+
RDL also provides a method `RDL.var_type` that can be used to force a local variable to have a single type through a method body, i.e., to treat it *flow-insensitively* like a standard type system:
|
635
641
|
|
636
642
|
```ruby
|
637
|
-
var_type :x, '
|
643
|
+
RDL.var_type :x, 'Integer'
|
638
644
|
x = 3 # okay
|
639
645
|
x = "three" # type error
|
640
646
|
```
|
641
647
|
|
642
|
-
The first argument to `var_type` is a symbol with the local variable name, and the second argument is a string containing the variable's type. Note that `var_type` is most useful at the beginning of method or code block. Using it elsewhere may result in surprising error messages, since RDL requires variables with fixed types to have the same type along all paths. Method parameters are treated as if `var_type` was called on them at the beginning of the method, fixing them to their declared type. This design choice may be revisited in the future.
|
648
|
+
The first argument to `RDL.var_type` is a symbol with the local variable name, and the second argument is a string containing the variable's type. Note that `RDL.var_type` is most useful at the beginning of method or code block. Using it elsewhere may result in surprising error messages, since RDL requires variables with fixed types to have the same type along all paths. Method parameters are treated as if `RDL.var_type` was called on them at the beginning of the method, fixing them to their declared type. This design choice may be revisited in the future.
|
643
649
|
|
644
650
|
There is one subtlety for local variables and code blocks. Consider the following code:
|
645
651
|
```ruby
|
@@ -649,11 +655,12 @@ m() { x = 'bar' }
|
|
649
655
|
```
|
650
656
|
If `m` invokes the code block, `x` will be a `String` after the call. Otherwise `x` will be `1`. Since RDL can't tell whether the code block is ever called, it assigns `x` type `1 or String`. It's actually quite tricky to do very precise reasoning about code blocks. For example, `m` could (pathologically) store its block in a global variable and then only call it the second time `m` is invoked. To keep its reasoning simple, RDL treats any local variables captured (i.e., imported from an outer scope) by a code block flow-insensitively for the lifetime of the method. The type of any such local variable is the union of all types that are ever assigned to it.
|
651
657
|
|
652
|
-
RDL always treats instance, class, and global variables flow-insensitively, hence their types must be defined with `var_type
|
658
|
+
RDL always treats instance, class, and global variables flow-insensitively, hence their types must be defined with `var_type`. In this case, `var_type` can optionally be accessed without the `RDL` prefix by adding in the annotation syntax:
|
653
659
|
|
654
660
|
```ruby
|
655
661
|
class A
|
656
|
-
|
662
|
+
extend RDL::Annotate
|
663
|
+
var_type :@f, 'Integer'
|
657
664
|
def m
|
658
665
|
@f = 3 # type safe
|
659
666
|
@f = "three" # type error, incompatible type in assignment
|
@@ -664,13 +671,13 @@ end
|
|
664
671
|
|
665
672
|
The `var_type` method may also be called as `var_type klass, :name, typ` to assign a type to an instance or class variable of class `klass`.
|
666
673
|
|
667
|
-
As a short-hand, RDL defines methods `attr_accessor_type`, `attr_reader_type`, and `attr_writer_type` to behave like their corresponding non-`_type` analogs but assign types to the attributes. For example, `attr_accessor_type :f, '
|
674
|
+
As a short-hand, RDL defines methods `attr_accessor_type`, `attr_reader_type`, and `attr_writer_type` (also part of `RDL::Annotate`) to behave like their corresponding non-`_type` analogs but assign types to the attributes. For example, `attr_accessor_type :f, 'Integer', :g, 'String'` is equivalent to:
|
668
675
|
|
669
676
|
```ruby
|
670
|
-
var_type :@f, '
|
677
|
+
var_type :@f, 'Integer'
|
671
678
|
var_type :@g, 'String'
|
672
|
-
type :f, '() ->
|
673
|
-
type :f=, '(
|
679
|
+
type :f, '() -> Integer'
|
680
|
+
type :f=, '(Integer) -> Integer'
|
674
681
|
type :g, '() -> String'
|
675
682
|
type :g=, '(String) -> String'
|
676
683
|
```
|
@@ -687,7 +694,7 @@ a, b = x # a has type 1, b has type String
|
|
687
694
|
RDL also allows a tuple `[t1, ..., tn]` to be used where `Array<t1 or ... or tn>` is expected. This means both when a tuple is passed to an `Array` position, and when any method is invoked on the tuple (even if RDL could safely apply that method to the tuple; this may change in the future):
|
688
695
|
|
689
696
|
```ruby
|
690
|
-
var_type @f, 'Array<
|
697
|
+
var_type @f, 'Array<Integer or String>'
|
691
698
|
@f = [1, 'foo'] # okay
|
692
699
|
@f.length # also okay
|
693
700
|
```
|
@@ -698,21 +705,21 @@ To maintain soundness, a tuple that is used as an `Array` is treated as if it we
|
|
698
705
|
x = [1, 'foo'] # at this point, x has type [1, String]
|
699
706
|
var_type @f, '[1, String]'
|
700
707
|
@f = x # okay so far
|
701
|
-
var_type @g, 'Array<
|
708
|
+
var_type @g, 'Array<Integer or String>'
|
702
709
|
@g = x # uh oh
|
703
710
|
```
|
704
711
|
|
705
|
-
When RDL encounters the assignment to `@g`, it retroactively changes `x` to have type `Array<
|
712
|
+
When RDL encounters the assignment to `@g`, it retroactively changes `x` to have type `Array<Integer or String>`, which is incompatible with type `[1, String]` of `@f`, so the assignment to `@g` signals an error.
|
706
713
|
|
707
714
|
RDL uses the same approach for hashes: hash literals are treated as finite hashes. A finite hash `{k1=>v1, ..., kn=>vn}` can be used where `Hash<k1 or ... or kn, v1 or ... or vn>` is expected. And if a finite hash is used as a `Hash` (including invoking methods on the finite hash; this may change in the future), then it is retroactively converted to a `Hash`.
|
708
715
|
|
709
716
|
## Other Features and Limitations
|
710
717
|
|
711
|
-
*Displaying types.* As an aid to debugging, the method `
|
718
|
+
*Displaying types.* As an aid to debugging, the method `RDL.note_type e` will display the type of `e` during type checking. At run time, this method returns its argument. Note that in certain cases RDL may type check the same code repeatedly, in which case an expression's type could be printed multiple times.
|
712
719
|
|
713
720
|
* *Conditional guards and singletons.* If an `if` or `unless` guard has a singleton type, RDL will typecheck both branches but not include types from the unrealizable branch in the expression type. For example, `if true then 1 else 'two' end` has type `1`. RDL behaves similarly for `&&` and `||`. However, RDL does not implement this logic for `case`.
|
714
721
|
|
715
|
-
* *Case analysis by class.* If the guard of a `case` statement is a variable, and then `when` branches compare against classes, RDL refines the type of the guard to be those classes within the corresponding `when` branch. For example, in `case x when
|
722
|
+
* *Case analysis by class.* If the guard of a `case` statement is a variable, and then `when` branches compare against classes, RDL refines the type of the guard to be those classes within the corresponding `when` branch. For example, in `case x when Integer ...(1)... when String ...(2)... end`, RDL will assume `x` is an `Integer` within `(1)` and a `String` within `(2)`.
|
716
723
|
|
717
724
|
* *Multiple Assignment and nil.* In Ruby, extra left-hand sides of multiple assignments are set to `nil`, e.g., `x, y = [1]` sets `x` to `1` and `y` to `nil`. However, RDL reports an error in this case; this may change in the future.
|
718
725
|
|
@@ -720,7 +727,7 @@ RDL uses the same approach for hashes: hash literals are treated as finite hashe
|
|
720
727
|
|
721
728
|
* *Caching.* If `typecheck: :call` is specified on a method, Ruby will type check the method every time it is called. In the future, RDL will cache these checks.
|
722
729
|
|
723
|
-
* *Dependent Types.* RDL ignores refinements in checking code with dependent types. E.g., given
|
730
|
+
* *Dependent Types.* RDL ignores refinements in checking code with dependent types. E.g., given an `Integer x {{ x > 0 }}`, RDL will simply treat `x` as an `Integer` and ignore the requirement that it be positive.
|
724
731
|
|
725
732
|
* *Unsupported Features.* There are several features of Ruby that are currently not handled by RDL. Here is a non-exhaustive list:
|
726
733
|
* `super` is not supported.
|
@@ -735,22 +742,52 @@ RDL's static type checker makes some assumptions that should hold unless your Ru
|
|
735
742
|
|
736
743
|
* `Class#===` is not redefined
|
737
744
|
* `Proc#call` is not redefined
|
745
|
+
* `Object#class` is not redefined
|
738
746
|
|
739
747
|
(More assumptions will be added here as they are added to RDL...)
|
740
748
|
|
741
|
-
#
|
749
|
+
# RDL Method Reference
|
750
|
+
|
751
|
+
The following methods are available in `RDL::Annotate`.
|
752
|
+
|
753
|
+
* `pre [klass], [meth], &blk, wrap: true, version: nil` - add `blk` as a precondition contract on `klass#meth`. If `klass` is omitted, applies to `self`. If `meth` is also omitted, applies to next defined method. If `wrap` is true, wrap the method to actually check the precondition. If it's false, don't wrap the method (this is probably useless). If a `version` string or array of strings is specified (in rubygems format), only apply when the current Ruby version matches.
|
754
|
+
|
755
|
+
* `post [klass], [meth], &blk, wrap: true, version: nil` - same as `pre`, but add a postcondition.
|
756
|
+
|
757
|
+
* `type [klass], [meth], typ, wrap: true, typecheck: nil, version: nil` - same as `pre`, but add a type specification. If `typecheck` is `nil`, does no static type checking. If it's `:call`, will type check the method each time it's called. If it's `:now`, will type check the method after it's defined. If it's some other `symbol`, will type check the method when `RDL.do_typecheck symbol` is called.
|
758
|
+
|
759
|
+
* `var_type [klass], var, typ` - indicate the `typ` is the type for `var`, which may be a `:@field` or `:local_variable`.
|
760
|
+
|
761
|
+
* `attr_accessor_type :name1, typ1, ...` calls `attr_accessor :name1, ...` and creates type annotations for the given field and its getters and setters.
|
762
|
+
|
763
|
+
* `attr_reader_type`, `attr_type`, and `attr_writer_type` - analogous to `attr_accessor_type`
|
764
|
+
|
765
|
+
* `rdl_alias [klass], new_name, old_name` tells RDL that method `new_name` of `klass` is an alias for method `old_name` (of the same class), and therefore they should have the same contracts and types. This method is only needed when adding contracts and types to method that have already been aliased; it's not needed if the method is aliased after the contract or type has been added. If the `klass` argument is omitted it's assumed to be `self`.
|
766
|
+
|
767
|
+
* `type_params [klass] [:param1, ...], :all, variance: nil, [&blk]` - indicates that `klass` should be treated as a generic type with parameters `:param1...`. The `:all` argument names a method of `klass` that iterates through a `klass` instance's contents. Alternatively, if a block is passed as an argument, that block is used as the iterator. The `variance` argument gives an array of variances of the parameters, `:+` for covariant, `:-` for contravaraiant, and `:~` for invariant. If `variance` is omitted, the parameters are assumed to be invariant.
|
768
|
+
|
769
|
+
The methods above can also be accessed as `rdl_method` after `extend RDL::RDLAnnotate`. They can also be accessed as `RDL.method`, but when doing so, however, the `klass` and `meth` arguments are not optional and must be specified. The `RDL` module also includes several other useful methods:
|
770
|
+
|
771
|
+
* `RDL.type_alias '%name', typ` - indicates that if `%name` appears in a type annotations, it should be expanded to `typ`.
|
772
|
+
|
773
|
+
* `RDL.nowrap [klass]`, if called at the top-level of a class, causes RDL to behave as if `wrap: false` were passed to all `type`, `pre`, and `post` calls in `klass`. This is mostly used for the core and standard libraries, which have trustworthy behavior hence enforcing their types and contracts is not worth the overhead. If `klass` is omitted it's assumed to be `self`.
|
774
|
+
|
775
|
+
* `RDL.do_typecheck(sym)` statically type checks all methods whose type annotations include argument `typecheck: sym`.
|
776
|
+
|
777
|
+
* `RDL.at(sym, &blk)` invokes `blk.call(sym)` when `RDL.do_typecheck(sym)` is called. Useful when type annotations need to be generated at some later time, e.g., because not all classes are loaded.
|
778
|
+
|
779
|
+
* `RDL.note_type e` - evaluates `e` and returns it at run time. During static type checking, prints out the type of `e`.
|
742
780
|
|
743
|
-
RDL
|
781
|
+
* `RDL.remove_type klass, meth` removes the type annotation for meth in klass. Fails if meth does not have a type annotation.
|
744
782
|
|
745
|
-
* `
|
783
|
+
* `RDL.insantiate!(var, typ1, ...)` - `var` must have a generic type. Instantiates the type of `x` with type parameters `typ1`, ...
|
746
784
|
|
747
|
-
* `
|
785
|
+
* `RDL.deinsantiate!(var)` - remove `var`'s instantiation.
|
748
786
|
|
749
|
-
* `
|
787
|
+
* `RDL.type_cast(e, typ, force: false)` - evaluate `e` and return it at run time. During dynamic contract checking, the returned object will have `typ` associated with it. If `force` is `false`, will also check that `e`'s run-time type is compatible with `typ`. If `force` is true then `typ` will be applied regardless of `e`'s type. During static type checking, the type checker will treat the object returned by `type_cast` has having type `typ`.
|
750
788
|
|
751
|
-
* `
|
789
|
+
* `RDL.query` prints information about types; see below for details.
|
752
790
|
|
753
|
-
* `rdl_do_at(sym, &blk)` invokes `blk.call(sym)` when `rdl_do_typecheck(sym)` is called. Useful when type annotations need to be generated at some later time, e.g., because not all classes are loaded.
|
754
791
|
|
755
792
|
# Performance
|
756
793
|
|
@@ -764,9 +801,9 @@ RDL supports some tradeoffs between safety and performance. There are three main
|
|
764
801
|
|
765
802
|
For uses of `pre` and `post`, there's not a lot of choice: those contracts are enforced at run-time and will incur the costs of wrapped methods. However, note that any methods that are not annotated with `pre` or `post` will not incur the cost of wrapping. (Similarly, methods not annotated with `type` never incur any wrapping cost.)
|
766
803
|
|
767
|
-
For uses of `type`, there are more choices, which can be split into two main use cases. First, suppose there's a method `m` that we want a type for but don't want to type check (for example, it may come from some external library). So suppose we read the documentation and give `m` type `(
|
804
|
+
For uses of `type`, there are more choices, which can be split into two main use cases. First, suppose there's a method `m` that we want a type for but don't want to type check (for example, it may come from some external library). So suppose we read the documentation and give `m` type `(Integer) -> Integer`. We now have to decide whether to wrap `m`. If we don't wrap `m`, then we incur no overhead on calls to `m`, but we are trusting the type. If we do wrap `m`, then on every call to it RDL will check that we call it with an `Integer` and it actually returns an `Integer`. So if we're not completely sure of `m`'s type, it might be useful to wrap it and then run test cases against it to see if the type annotation is every violated. (For example, the RDL developers did this to test out many of the core library annotations in RDL.)
|
768
805
|
|
769
|
-
Second, suppose there's a method `m` that we do want to type check, and again `m` has type `(
|
806
|
+
Second, suppose there's a method `m` that we do want to type check, and again `m` has type `(Integer) -> Integer`. Now RDL will use type checking to ensure that if `m` is given an `Integer` then it returns `Integer`. But now we again have to decide whether to wrap `m`. If we don't wrap `m`, then we get the most efficiency, since typechecking (assuming we do not do it at calls) will only happen once and calls will incur no overhead. On the other hand, it could be that some non-typechecked code calls `m` with something that's not an `Integer`, in which case `m` might do anything, including report a type error. (Notice the type checking of `m` assumed its input was an `Integer`, and it doesn't say anything about the case when its argument is not.) To protect against this case, we can wrap `m`. Then if a caller violates `m`'s type, we'll get an error in the caller code when it tries to call `m`.
|
770
807
|
|
771
808
|
(Side note: If typed methods are wrapped, then their type contracts are checked at run time for *all* callers, including ones that are were statically type checked and hence couldn't call methods at incorrect types. A future version of RDL will fix this, but it will require some significant changes to RDL's implementation strategy.)
|
772
809
|
|
@@ -800,9 +837,9 @@ $ rdl_query Array
|
|
800
837
|
* Methods can also be search for by their type signature:
|
801
838
|
|
802
839
|
```shell
|
803
|
-
$ rdl_query "(
|
804
|
-
BigDecimal.limit: (
|
805
|
-
Dir#pos=: (
|
840
|
+
$ rdl_query "(Integer) -> Integer" # print all methods of type (Integer) -> Integer
|
841
|
+
BigDecimal.limit: (Integer) -> Integer
|
842
|
+
Dir#pos=: (Integer) -> Integer
|
806
843
|
... and a lot more
|
807
844
|
```
|
808
845
|
|
@@ -810,15 +847,15 @@ The type signature uses the standard RDL syntax, with two extensions: `.` can be
|
|
810
847
|
|
811
848
|
```shell
|
812
849
|
$ rdl_query "(.) -> ." # methods that take one argument and return anything
|
813
|
-
$ rdl_query "(
|
814
|
-
$ rdl_query "(
|
815
|
-
$ rdl_query "(...,
|
816
|
-
$ rdl_query "(...,
|
817
|
-
$ rdl_query "(
|
850
|
+
$ rdl_query "(Integer, .) -> ." # methods that take two arguments, the first of which is an Integer
|
851
|
+
$ rdl_query "(Integer, ...) -> ." # methods whose first argument is an Integer
|
852
|
+
$ rdl_query "(..., Integer) -> ." # methods whose last argument is an Integer
|
853
|
+
$ rdl_query "(..., Integer, ...) -> ." # methods that take an Integer somewhere
|
854
|
+
$ rdl_query "(Integer or .) -> ." # methods that take a single argument that is a union containing an Integer
|
818
855
|
$ rdl_query "(.?) -> ." # methods that take one, optional argument
|
819
856
|
```
|
820
857
|
|
821
|
-
Note that aside from `.` and `...`, the matching is exact. For example `(
|
858
|
+
Note that aside from `.` and `...`, the matching is exact. For example `(Integer) -> Integer` will not match a method of type `(Integer or String) -> Integer`.
|
822
859
|
|
823
860
|
# Configuration
|
824
861
|
|
@@ -882,7 +919,7 @@ In Object-Oriented Program Languages and Systems (OOPS) Track at ACM Symposium o
|
|
882
919
|
|
883
920
|
# Copyright
|
884
921
|
|
885
|
-
Copyright (c) 2014-
|
922
|
+
Copyright (c) 2014-2017, University of Maryland, College Park. All rights reserved.
|
886
923
|
|
887
924
|
# Authors
|
888
925
|
|