rdl 2.0.0.rc4 → 2.0.0.rc5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +56 -84
- data/lib/rdl/boot.rb +4 -0
- data/lib/rdl/boot_rails.rb +3 -1
- data/lib/rdl/contracts/flat.rb +1 -2
- data/lib/rdl/typecheck.rb +138 -27
- data/lib/rdl/types/annotated_arg.rb +8 -0
- data/lib/rdl/types/finite_hash.rb +32 -17
- data/lib/rdl/types/lexer.rex +1 -0
- data/lib/rdl/types/lexer.rex.rb +3 -0
- data/lib/rdl/types/method.rb +24 -4
- data/lib/rdl/types/optional.rb +8 -1
- data/lib/rdl/types/parser.racc +12 -9
- data/lib/rdl/types/parser.tab.rb +293 -255
- data/lib/rdl/types/type.rb +20 -11
- data/lib/rdl/types/union.rb +3 -0
- data/lib/rdl/types/vararg.rb +7 -2
- data/lib/rdl/wrap.rb +15 -2
- data/lib/types/core-ruby-2.x/proc.rb +16 -0
- data/lib/types/rails-5.x/_helpers.rb +20 -0
- data/lib/types/rails-5.x/active_model/errors.rb +15 -0
- data/lib/types/rails-5.x/active_model/validations.rb +5 -0
- data/lib/types/rails-5.x/active_record/associations.rb +190 -0
- data/lib/types/rails-5.x/active_record/core.rb +3 -0
- data/lib/types/rails-5.x/active_record/model_schema.rb +7 -10
- data/rdl.gemspec +2 -2
- data/test/test_le.rb +24 -0
- data/test/test_member.rb +24 -2
- data/test/test_parser.rb +21 -10
- data/test/test_query.rb +5 -2
- data/test/test_type_contract.rb +34 -2
- data/test/test_typecheck.rb +296 -36
- data/test/test_types.rb +8 -4
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37bb60eb1678b69aca3b6967c386f31c2d0c882f
|
4
|
+
data.tar.gz: 8824ba76d0daa9d4049df6d4677846386e5c6733
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 593c30a12e453dc79c796b65c8598c345b0bb562978fb4a21fcb903c9a8d0fffcbc0b0534bb0187933ae8399c9dc8022bac689c77601282f442bb687dc79d985
|
7
|
+
data.tar.gz: 4daaeafc5cc639db5afe714c9b4c71a8f9455dea7d8151741f3dcd914f384ca728389cc9f1931a255e97513be9b2423c7a0773dc09eba8959905fdfc6a568658
|
data/README.md
CHANGED
@@ -49,23 +49,7 @@
|
|
49
49
|
|
50
50
|
# Introduction
|
51
51
|
|
52
|
-
RDL is a lightweight system for adding
|
53
|
-
|
54
|
-
```ruby
|
55
|
-
require 'rdl'
|
56
|
-
|
57
|
-
pre { |x| x > 0 }
|
58
|
-
post { |r,x| r > 0 }
|
59
|
-
def sqrt(x)
|
60
|
-
# return the square root of x
|
61
|
-
end
|
62
|
-
```
|
63
|
-
|
64
|
-
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...)
|
65
|
-
|
66
|
-
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`.
|
67
|
-
|
68
|
-
In addition to arbitrary pre- and post-conditions, RDL also has extensive support for contracts that are *types*. For example, we can write the following in RDL:
|
52
|
+
RDL is a lightweight system for adding types, type checking, and contracts to Ruby. In RDL, *types* can be used to decorate methods:
|
69
53
|
|
70
54
|
```ruby
|
71
55
|
require 'rdl'
|
@@ -74,9 +58,9 @@ type '(Fixnum, Fixnum) -> String'
|
|
74
58
|
def m(x,y) ... end
|
75
59
|
```
|
76
60
|
|
77
|
-
This indicates that `m`
|
61
|
+
This indicates that `m` returns a `String` if given two `Fixnum` 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 `Fixnums`, and that `m` returns an instance of `String`.
|
78
62
|
|
79
|
-
|
63
|
+
RDL can also *statically type check* method bodies against their signatures. For example:
|
80
64
|
|
81
65
|
```ruby
|
82
66
|
file.rb:
|
@@ -98,7 +82,9 @@ $ ruby file.rb
|
|
98
82
|
|
99
83
|
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.
|
100
84
|
|
101
|
-
RDL
|
85
|
+
RDL supports many more complex type annotations; see below for a complete discussion and examples.
|
86
|
+
|
87
|
+
RDL types are stored in memory at run time, so it's also possible for programs to query them. RDL includes lots of contracts and types for the core and standard libraries. Since those methods are generally trustworthy, RDL doesn't actually enforce the contracts (since that would add overhead), but they are available to search, query, and use during type checking. RDL includes a small script `rdl_query` to look up type information from the command line. Note you might need to put the argument in quotes depending on your shell.
|
102
88
|
|
103
89
|
```shell
|
104
90
|
$ rdl_query String#include? # print type for instance method of another class
|
@@ -122,7 +108,19 @@ $ irb
|
|
122
108
|
> rdl_query '...' # as above
|
123
109
|
```
|
124
110
|
|
125
|
-
|
111
|
+
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:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
require 'rdl'
|
115
|
+
|
116
|
+
pre { |x| x > 0 }
|
117
|
+
post { |r,x| r > 0 }
|
118
|
+
def sqrt(x)
|
119
|
+
# return the square root of x
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
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 `rdl_query`.
|
126
124
|
|
127
125
|
# Using RDL
|
128
126
|
|
@@ -136,11 +134,11 @@ RDL currently supports Ruby 2.x. It may or may not work with other versions.
|
|
136
134
|
|
137
135
|
## Loading RDL
|
138
136
|
|
139
|
-
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 '
|
137
|
+
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'`. This will load the types based on the current `RUBY_VERSION`. Currently RDL has types for the following versions of Ruby:
|
140
138
|
|
141
139
|
* 2.x
|
142
140
|
|
143
|
-
(Currently all
|
141
|
+
(Currently all 2.x versions are assumed to have the same library type signatures, which may not be correct.)
|
144
142
|
|
145
143
|
## Disabling RDL
|
146
144
|
|
@@ -164,13 +162,18 @@ to get the head version from github.
|
|
164
162
|
|
165
163
|
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`).
|
166
164
|
|
167
|
-
|
165
|
+
**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.
|
166
|
+
|
167
|
+
Currently, RDL has types for the following versions of Rails:
|
168
168
|
|
169
169
|
* Rails 5.x support - limited to the following:
|
170
|
-
*
|
171
|
-
*
|
172
|
-
|
173
|
-
|
170
|
+
* Models
|
171
|
+
* Generates type annotations for model column getters and setters
|
172
|
+
* `find_by` and `find_by!`
|
173
|
+
* Generates types annotations for methods added by `belongs_to`, `has_one`, `has_many`, `has_and_belongs_to_many`.
|
174
|
+
* Note currently RDL doesn't do very precise type checking of relations
|
175
|
+
* Controllers
|
176
|
+
* `errors`
|
174
177
|
|
175
178
|
## Preconditions and Postconditions
|
176
179
|
|
@@ -559,6 +562,12 @@ type MyClass, :foo, '(a: Fixnum, b: String) { () -> %any } -> %any'
|
|
559
562
|
```
|
560
563
|
Here `foo`, takes a hash where key `:a` is mapped to a `Fixnum` and key `:b` is mapped to a `String`. Similarly, `{'a'=>Fixnum, 2=>String}` types a hash where keys `'a'` and `2` are mapped to a `Fixnum` and `String`, respectively. Both syntaxes can be used to define hash types.
|
561
564
|
|
565
|
+
RDL also allows a "rest" type in finite hashes (of course, they're not so finite if they use it!):
|
566
|
+
```ruby
|
567
|
+
type MyClass, :foo, '(a: Fixnum, b: String, **Float) -> %any'
|
568
|
+
```
|
569
|
+
In this method, `a` is a `Fixnum`, `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.
|
570
|
+
|
562
571
|
## Type Casts
|
563
572
|
|
564
573
|
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 `o.type_cast(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 `type_cast`, RDL will perform the cast without checking whether `o` is actually a member of the given type. For example, `x = "a".type_cast('nil', force: true)` will make RDL treat `x` as if it had type `nil`, even though it's a `String`.
|
@@ -577,23 +586,19 @@ Types can be prefixed with `!` to indicate the associated value is not `nil`. Fo
|
|
577
586
|
|
578
587
|
# Static Type Checking
|
579
588
|
|
580
|
-
|
589
|
+
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.
|
581
590
|
|
582
591
|
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.
|
583
592
|
|
584
593
|
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.
|
585
594
|
|
586
|
-
To perform type checking, RDL needs source code, which it gets by parsing the file containing the to-be-typechecked method. Hence, static type checking does not work in `irb` since RDL has no way of getting the source.
|
587
|
-
|
588
|
-
Typechecking does work in `pry` (this feature has only limited testing) as long as typechecking is delayed until after the method is defined:
|
595
|
+
To perform type checking, RDL needs source code, which it gets by parsing the file containing the to-be-typechecked method. Hence, static type checking does not work in `irb` since RDL has no way of getting the source. Typechecking does work in `pry` (this feature has only limited testing) as long as typechecking is delayed until after the method is defined:
|
589
596
|
|
590
597
|
```ruby
|
591
598
|
[2] pry(main)> require 'rdl'
|
592
|
-
[3] pry(main)> require '
|
599
|
+
[3] pry(main)> require 'types/core'
|
593
600
|
[4] pry(main)> type '() -> Fixnum', typecheck: :later # note: typecheck: :now doesn't work in pry
|
594
|
-
[5] pry(main)> def f
|
595
|
-
[5] pry(main)* 'haha'
|
596
|
-
[5] pry(main)* end
|
601
|
+
[5] pry(main)> def f; 'haha'; end
|
597
602
|
[6] pry(main)> rdl_do_typecheck :later
|
598
603
|
RDL::Typecheck::StaticTypeError:
|
599
604
|
(string):2:3: error: got type `String' where return type `Fixnum' expected
|
@@ -639,7 +644,7 @@ x = 1
|
|
639
644
|
m() { x = 'bar' }
|
640
645
|
# what is x's type here?
|
641
646
|
```
|
642
|
-
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
|
647
|
+
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.
|
643
648
|
|
644
649
|
RDL always treats instance, class, and global variables flow-insensitively, hence their types must be defined with `var_type`:
|
645
650
|
|
@@ -656,7 +661,7 @@ end
|
|
656
661
|
|
657
662
|
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`.
|
658
663
|
|
659
|
-
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
|
664
|
+
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, 'Fixnum', :g, 'String'` is equivalent to:
|
660
665
|
|
661
666
|
```ruby
|
662
667
|
var_type :@f, 'Fixnum'
|
@@ -694,16 +699,18 @@ var_type @g, 'Array<Fixnum or String>'
|
|
694
699
|
@g = x # uh oh
|
695
700
|
```
|
696
701
|
|
697
|
-
When RDL encounters the assignment to `@g`, it retroactively changes `x` to have type `Array<Fixnum or String>`, which is incompatible with type `[1, String]`, so the
|
702
|
+
When RDL encounters the assignment to `@g`, it retroactively changes `x` to have type `Array<Fixnum or String>`, which is incompatible with type `[1, String]` of `@f`, so the assignment to `@g` signals an error.
|
698
703
|
|
699
704
|
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`.
|
700
705
|
|
701
706
|
## Other Features and Limitations
|
702
707
|
|
703
|
-
*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.
|
708
|
+
*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.
|
704
709
|
|
705
710
|
* *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`.
|
706
711
|
|
712
|
+
* *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 Fixnum ...(1)... when String ...(2)... end`, RDL will assume `x` is a `Fixnum` within `(1)` and a `String` within `(2)`.
|
713
|
+
|
707
714
|
* *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.
|
708
715
|
|
709
716
|
* *Block formal arguments.* Similarly, RDL reports an error if a block is called with the wrong number of arguments even though Ruby does not signal an error in this case.
|
@@ -714,16 +721,17 @@ RDL uses the same approach for hashes: hash literals are treated as finite hashe
|
|
714
721
|
|
715
722
|
* *Unsupported Features.* There are several features of Ruby that are currently not handled by RDL. Here is a non-exhaustive list:
|
716
723
|
* `super` is not supported.
|
717
|
-
* `lambda` has special semantics for `return`; this is not supported.
|
718
|
-
* Only simple
|
724
|
+
* `lambda` has special semantics for `return`; this is not supported. Stabby lambda is also not supported.
|
725
|
+
* Only simple `for` iteration variables are supported.
|
719
726
|
* Control flow for exceptions is not analyzed fully soundly; some things are not reported as possibly `nil` that could be.
|
720
727
|
* Only simple usage of constants is handled.
|
721
728
|
|
722
729
|
## Assumptions
|
723
730
|
|
724
|
-
RDL makes some assumptions that should hold unless your Ruby code is doing something highly unusual:
|
731
|
+
RDL's static type checker makes some assumptions that should hold unless your Ruby code is doing something highly unusual. RDL assumes:
|
725
732
|
|
726
733
|
* `Class#===` is not redefined
|
734
|
+
* `Proc#call` is not redefined
|
727
735
|
|
728
736
|
(More assumptions will be added here as they are added to RDL...)
|
729
737
|
|
@@ -739,6 +747,8 @@ RDL also includes a few other useful methods:
|
|
739
747
|
|
740
748
|
* `rdl_query` prints information about types; see below for details.
|
741
749
|
|
750
|
+
* `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.
|
751
|
+
|
742
752
|
# Queries
|
743
753
|
|
744
754
|
As discussed above, RDL includes a small script, `rdl_query`, to look up type information. (Currently it does not support other pre- and postconditions.) The script takes a single argument, which should be a string. Note that when using the shell script, you may need to use quotes depending on your shell. Currently several queries are supported:
|
@@ -811,7 +821,7 @@ RDL supports the following configuration options:
|
|
811
821
|
|
812
822
|
# Bibliography
|
813
823
|
|
814
|
-
Here are some research papers we have written exploring
|
824
|
+
Here are some research papers we have written exploring types, contracts, and Ruby.
|
815
825
|
|
816
826
|
* Brianna M. Ren and Jeffrey S. Foster.
|
817
827
|
[Just-in-Time Static Type Checking for Dynamic Languages](http://www.cs.umd.edu/~jfoster/papers/pldi16.pdf).
|
@@ -863,44 +873,6 @@ Copyright (c) 2014-2016, University of Maryland, College Park. All rights reserv
|
|
863
873
|
* Alexander T. Yu
|
864
874
|
* Milod Kazerounian
|
865
875
|
|
866
|
-
#
|
867
|
-
|
868
|
-
* How to check whether initialize? is user-defined? method_defined? always
|
869
|
-
returns true, meaning wrapping isn't fully working with initialize.
|
870
|
-
|
871
|
-
* Currently if a NominalType name is expressed differently, e.g., A
|
872
|
-
vs. EnclosingClass::A, the types will be different when compared
|
873
|
-
with ==.
|
874
|
-
|
875
|
-
* Macros, %bool should really be %any
|
876
|
-
|
877
|
-
* Method types that are parametric themselves (not just ones that use
|
878
|
-
enclosing class parameters)
|
879
|
-
|
880
|
-
* Rails types
|
881
|
-
|
882
|
-
* Deferred contracts on new (watch for class addition)
|
883
|
-
|
884
|
-
* DSL contracts
|
885
|
-
|
886
|
-
* double-splat arguments, which bind to an arbitrary set of keywords
|
887
|
-
|
888
|
-
* included versus extended modules, e.g., Kernel is included in Object, so its
|
889
|
-
class methods become Object's instance methods
|
890
|
-
|
891
|
-
* Better story for different types for different Ruby versions
|
892
|
-
|
893
|
-
* Better query facility (more kinds of searches). Contract queries?
|
894
|
-
|
895
|
-
* Write documentation on: Raw Contracts and Types, RDL Configuration, Code Overview
|
896
|
-
|
897
|
-
* Structural type queries, allow name to be unknown; same with finite hash keys,
|
898
|
-
same with generic base types?
|
899
|
-
|
900
|
-
* Allow ... in named args list in queries
|
901
|
-
|
902
|
-
* Queries, include more regexp operators aside from . and ...
|
903
|
-
|
904
|
-
* Queries, allow regexp in class and method names; suggested by Andreas Adamcik, Vienna
|
876
|
+
# Discussion group
|
905
877
|
|
906
|
-
|
878
|
+
[RDL Users](https://groups.google.com/forum/#!forum/rdl-users)
|
data/lib/rdl/boot.rb
CHANGED
@@ -46,6 +46,9 @@ $__rdl_to_wrap = Set.new
|
|
46
46
|
$__rdl_to_typecheck = Hash.new
|
47
47
|
$__rdl_to_typecheck[:now] = Set.new
|
48
48
|
|
49
|
+
# Map from symbols to Array<Proc> where the Procs are called when those symbols are rdl_do_typecheck'd
|
50
|
+
$__rdl_at = Hash.new
|
51
|
+
|
49
52
|
# List of contracts that should be applied to the next method definition
|
50
53
|
$__rdl_deferred = []
|
51
54
|
|
@@ -122,6 +125,7 @@ $__rdl_symbol_type = RDL::Type::NominalType.new Symbol
|
|
122
125
|
$__rdl_range_type = RDL::Type::NominalType.new Range
|
123
126
|
$__rdl_regexp_type = RDL::Type::NominalType.new Regexp
|
124
127
|
$__rdl_standard_error_type = RDL::Type::NominalType.new StandardError
|
128
|
+
$__rdl_proc_type = RDL::Type::NominalType.new Proc
|
125
129
|
|
126
130
|
# Hash from special type names to their values
|
127
131
|
$__rdl_special_types = {'%any' => $__rdl_top_type,
|
data/lib/rdl/boot_rails.rb
CHANGED
@@ -7,7 +7,9 @@ if Rails.env.development? || Rails.env.test?
|
|
7
7
|
Dir[File.dirname(__FILE__) + "/../types/rails-#{dir}/**/*.rb"].each { |f| require f }
|
8
8
|
elsif Rails.env.production?
|
9
9
|
require 'rdl_disable'
|
10
|
-
|
10
|
+
class ActionController::Base
|
11
|
+
def self.params_type(typs); end
|
12
|
+
end
|
11
13
|
else
|
12
14
|
raise RuntimeError, "Don't know what to do in Rails environment #{Rails.env}"
|
13
15
|
end
|
data/lib/rdl/contracts/flat.rb
CHANGED
@@ -9,8 +9,7 @@ module RDL::Contract
|
|
9
9
|
|
10
10
|
def check(slf, *v, &blk)
|
11
11
|
$__rdl_contract_switch.off {
|
12
|
-
if
|
13
|
-
((@pred.arity < 0) ? (@pred.arity.abs - 1) <= v.size : @pred.arity == v.size)) then
|
12
|
+
if @pred && v.length >= @pred.arity
|
14
13
|
unless blk ? slf.instance_exec(*v, blk, &@pred) : slf.instance_exec(*v, &@pred) # TODO: Fix blk
|
15
14
|
# unless blk ? pred.call(*v, &blk) : pred.call(*v)
|
16
15
|
raise ContractError,
|
data/lib/rdl/typecheck.rb
CHANGED
@@ -2,7 +2,7 @@ module RDL::Typecheck
|
|
2
2
|
|
3
3
|
class StaticTypeError < StandardError; end
|
4
4
|
|
5
|
-
@@empty_hash_type = RDL::Type::FiniteHashType.new(Hash.new)
|
5
|
+
@@empty_hash_type = RDL::Type::FiniteHashType.new(Hash.new, nil)
|
6
6
|
@@asgn_to_var = { lvasgn: :lvar, ivasgn: :ivar, cvasgn: :cvar, gvasgn: :gvar }
|
7
7
|
|
8
8
|
# Create mapping from file/line numbers to the def that appears at that location
|
@@ -208,7 +208,6 @@ module RDL::Typecheck
|
|
208
208
|
raise RuntimeError, "Method #{name} defined where method #{meth} expected" if name.to_sym != meth
|
209
209
|
context_types = $__rdl_info.get(klass, meth, :context_types)
|
210
210
|
types.each { |type|
|
211
|
-
# TODO will need fancier logic here for matching up more complex arg lists
|
212
211
|
if RDL::Util.has_singleton_marker(klass)
|
213
212
|
# to_class gets the class object itself, so remove singleton marker to get class rather than singleton class
|
214
213
|
self_type = RDL::Type::SingletonType.new(RDL::Util.to_class(RDL::Util.remove_singleton_marker(klass)))
|
@@ -217,10 +216,7 @@ module RDL::Typecheck
|
|
217
216
|
end
|
218
217
|
inst = {self: self_type}
|
219
218
|
type = type.instantiate inst
|
220
|
-
|
221
|
-
error :arg_count_mismatch, ['method', type.args.length, 'method', args.children.length], (if args.children.empty? then ast else args end)
|
222
|
-
end
|
223
|
-
targs = args.children.map { |arg| arg.children[0] }.zip(type.args).to_h
|
219
|
+
_, targs = args_hash({}, Env.new, type, args, ast, 'method')
|
224
220
|
targs[:self] = self_type
|
225
221
|
scope = { tret: type.ret, tblock: type.block, captured: Hash.new, context_types: context_types }
|
226
222
|
begin
|
@@ -236,6 +232,79 @@ module RDL::Typecheck
|
|
236
232
|
$__rdl_info.set(klass, meth, :typechecked, true)
|
237
233
|
end
|
238
234
|
|
235
|
+
# [+ scope +] is used to typecheck default values for optional arguments
|
236
|
+
# [+ env +] is used to typecheck default values for optional arguments
|
237
|
+
# [+ type +] is a MethodType
|
238
|
+
# [+ args +] is an `args` node from the AST
|
239
|
+
# [+ ast +] is where to report an error if `args` is empty
|
240
|
+
# [+ kind +] is either `'method'` or `'block'`, and is only used for printing error messages
|
241
|
+
# Returns a Hash<Symbol, Type> mapping formal argument names to their types
|
242
|
+
def self.args_hash(scope, env, type, args, ast, kind)
|
243
|
+
targs = Hash.new
|
244
|
+
tpos = 0 # position in type.args
|
245
|
+
kw_args_matched = []
|
246
|
+
kw_rest_matched = false
|
247
|
+
args.children.each { |arg|
|
248
|
+
error :type_args_fewer, [kind, kind], arg if tpos >= type.args.length && arg.type != :blockarg # blocks could be called with yield
|
249
|
+
targ = type.args[tpos]
|
250
|
+
if arg.type == :arg
|
251
|
+
error :type_arg_kind_mismatch, [kind, 'optional', 'required'], arg if targ.optional?
|
252
|
+
error :type_arg_kind_mismatch, [kind, 'vararg', 'required'], arg if targ.vararg?
|
253
|
+
targs[arg.children[0]] = targ
|
254
|
+
tpos += 1
|
255
|
+
elsif arg.type == :optarg
|
256
|
+
error :type_arg_kind_mismatch, [kind, 'vararg', 'optional'], arg if targ.vararg?
|
257
|
+
error :type_arg_kind_mismatch, [kind, 'required', 'optional'], arg if !targ.optional?
|
258
|
+
env, default_type = tc(scope, env, arg.children[1])
|
259
|
+
error :optional_default_type, [default_type, targ.type], arg.children[1] unless default_type <= targ.type
|
260
|
+
targs[arg.children[0]] = targ.type
|
261
|
+
tpos += 1
|
262
|
+
elsif arg.type == :restarg
|
263
|
+
error :type_arg_kind_mismatch, [kind, 'optional', 'vararg'], arg if targ.optional?
|
264
|
+
error :type_arg_kind_mismatch, [kind, 'required', 'vararg'], arg if !targ.vararg?
|
265
|
+
targs[arg.children[0]] = RDL::Type::GenericType.new($__rdl_array_type, targ.type)
|
266
|
+
tpos += 1
|
267
|
+
elsif arg.type == :kwarg
|
268
|
+
error :type_args_no_kws, [kind], arg unless targ.is_a?(RDL::Type::FiniteHashType)
|
269
|
+
kw = arg.children[0]
|
270
|
+
error :type_args_no_kw, [kind, kw], arg unless targ.elts.has_key? kw
|
271
|
+
tkw = targ.elts[kw]
|
272
|
+
error :type_args_kw_mismatch, [kind, 'optional', kw, 'required'], arg if tkw.is_a? RDL::Type::OptionalType
|
273
|
+
kw_args_matched << kw
|
274
|
+
targs[kw] = tkw
|
275
|
+
elsif arg.type == :kwoptarg
|
276
|
+
error :type_args_no_kws, [kind], arg unless targ.is_a?(RDL::Type::FiniteHashType)
|
277
|
+
kw = arg.children[0]
|
278
|
+
error :type_args_no_kw, [kind, kw], arg unless targ.elts.has_key? kw
|
279
|
+
tkw = targ.elts[kw]
|
280
|
+
error :type_args_kw_mismatch, [kind, 'required', kw, 'optional'], arg if !tkw.is_a?(RDL::Type::OptionalType)
|
281
|
+
env, default_type = tc(scope, env, arg.children[1])
|
282
|
+
error :optional_default_kw_type, [kw, default_type, tkw.type], arg.children[1] unless default_type <= tkw.type
|
283
|
+
kw_args_matched << kw
|
284
|
+
targs[kw] = tkw.type
|
285
|
+
elsif arg.type == :kwrestarg
|
286
|
+
error :type_args_no_kws, [kind], e unless targ.is_a?(RDL::Type::FiniteHashType)
|
287
|
+
error :type_args_no_kw_rest, [kind], arg if targ.rest.nil?
|
288
|
+
targs[arg.children[0]] = RDL::Type::GenericType.new($__rdl_hash_type, $__rdl_symbol_type, targ.rest)
|
289
|
+
kw_rest_matched = true
|
290
|
+
elsif arg.type == :blockarg
|
291
|
+
error :type_arg_block, [kind, kind], arg unless type.block
|
292
|
+
targs[arg.children[0]] = type.block
|
293
|
+
# Note no check that if type.block then method expects block, because blocks can be called with yield
|
294
|
+
else
|
295
|
+
error :generic_error, ["Don't know what to do with actual argument of type #{arg.type}"], arg
|
296
|
+
end
|
297
|
+
}
|
298
|
+
if (tpos == type.args.length - 1) && type.args[tpos].is_a?(RDL::Type::FiniteHashType)
|
299
|
+
rest = type.args[tpos].elts.keys - kw_args_matched
|
300
|
+
error :type_args_kw_more, [kind, rest.map { |s| s.to_s }.join(", "), kind], ast unless rest.empty?
|
301
|
+
error :type_args_kw_rest, [kind], ast unless kw_rest_matched || type.args[tpos].rest.nil?
|
302
|
+
else
|
303
|
+
error :type_args_more, [kind, kind], (if args.children.empty? then ast else args end) if type.args.length != tpos
|
304
|
+
end
|
305
|
+
return [env, targs]
|
306
|
+
end
|
307
|
+
|
239
308
|
# The actual type checking logic.
|
240
309
|
# [+ scope +] tracks flow-insensitive information about the current scope, excluding local variables
|
241
310
|
# [+ env +] is the (local variable) Env
|
@@ -339,7 +408,7 @@ module RDL::Typecheck
|
|
339
408
|
if is_fh
|
340
409
|
# keys are all symbols
|
341
410
|
fh = tlefts.map { |t| t.val }.zip(trights).to_h
|
342
|
-
[envi, RDL::Type::FiniteHashType.new(fh)]
|
411
|
+
[envi, RDL::Type::FiniteHashType.new(fh, nil)]
|
343
412
|
else
|
344
413
|
tleft = RDL::Type::UnionType.new(*tlefts)
|
345
414
|
tright = RDL::Type::UnionType.new(*trights)
|
@@ -507,6 +576,12 @@ module RDL::Typecheck
|
|
507
576
|
else
|
508
577
|
error :cant_splat, [ti], ei.children[0]
|
509
578
|
end
|
579
|
+
elsif ei.type == :block_pass
|
580
|
+
raise RuntimeError, "impossible to pass block arg and literal block" if scope[:block]
|
581
|
+
envi, ti = tc(sscope, envi, ei.children[0])
|
582
|
+
# convert using to_proc if necessary
|
583
|
+
ti = tc_send(sscope, envi, ti, :to_proc, [], nil, ei) unless ti.is_a? RDL::Type::MethodType
|
584
|
+
block = [ti, ei]
|
510
585
|
else
|
511
586
|
envi, ti = tc(sscope, envi, ei)
|
512
587
|
tactuals << ti
|
@@ -918,7 +993,7 @@ RUBY
|
|
918
993
|
# [+ trecvs +] is the type of the recevier
|
919
994
|
# [+ meth +] is a symbol with the method name
|
920
995
|
# [+ tactuals +] are the actual arguments
|
921
|
-
# [+ block +] is a pair of expressions [block-args, block-body], from the block AST node
|
996
|
+
# [+ block +] is a pair of expressions [block-args, block-body], from the block AST node OR [block-type, block-arg-AST-node]
|
922
997
|
# [+ e +] is the expression at which location to report an error
|
923
998
|
def self.tc_send(scope, env, trecvs, meth, tactuals, block, e)
|
924
999
|
scope[:exn] = Env.join(e, scope[:exn], env) if scope.has_key? :exn # assume this call might raise an exception
|
@@ -947,6 +1022,16 @@ RUBY
|
|
947
1022
|
error :no_singleton_method_type, [trecv.val, meth], e unless ts
|
948
1023
|
inst = {self: trecv}
|
949
1024
|
tmeth_inter = ts.map { |t| t.instantiate(inst) }
|
1025
|
+
elsif trecv.val.is_a?(Symbol) && meth == :to_proc
|
1026
|
+
# Symbol#to_proc on a singleton symbol type produces a Proc for the method of the same name
|
1027
|
+
if env[:self].is_a?(RDL::Type::NominalType)
|
1028
|
+
klass = env[:self].klass
|
1029
|
+
else # SingletonType(class)
|
1030
|
+
klass = env[:self].val
|
1031
|
+
end
|
1032
|
+
ts = lookup(scope, klass.to_s, trecv.val, e)
|
1033
|
+
error :no_type_for_symbol, [trecv.val.inspect], e if ts.nil?
|
1034
|
+
return ts
|
950
1035
|
else
|
951
1036
|
klass = trecv.val.class.to_s
|
952
1037
|
ts = lookup(scope, klass, meth, e)
|
@@ -970,6 +1055,14 @@ RUBY
|
|
970
1055
|
tmeth_inter = ts.map { |t| t.instantiate(inst) }
|
971
1056
|
when RDL::Type::VarType
|
972
1057
|
error :recv_var_type, [trecv], e
|
1058
|
+
when RDL::Type::MethodType
|
1059
|
+
if meth == :call
|
1060
|
+
# Special case - invokes the Proc
|
1061
|
+
tmeth_inter = [trecv]
|
1062
|
+
else
|
1063
|
+
# treat as Proc
|
1064
|
+
tc_send_one_recv(scope, env, $__rdl_proc_type, meth, tactuals, block, e)
|
1065
|
+
end
|
973
1066
|
else
|
974
1067
|
raise RuntimeError, "receiver type #{trecv} not supported yet"
|
975
1068
|
end
|
@@ -1006,12 +1099,14 @@ Actual arg type#{tactuals.size > 1 ? "s" : ""}:
|
|
1006
1099
|
(#{tactuals.map { |ti| ti.to_s }.join(', ')}) #{if block then '{ block }' end}
|
1007
1100
|
RUBY
|
1008
1101
|
msg.chomp! # remove trailing newline
|
1009
|
-
name = if
|
1102
|
+
name = if trecv.is_a?(RDL::Type::SingletonType) && trecv.val.is_a?(Class) && (meth == :new) then
|
1010
1103
|
:initialize
|
1011
1104
|
elsif trecv.is_a? RDL::Type::SingletonType
|
1012
1105
|
trecv.val.class.to_s
|
1013
|
-
elsif
|
1106
|
+
elsif trecv.is_a?(RDL::Type::NominalType) || trecv.is_a?(RDL::Type::GenericType)
|
1014
1107
|
trecv.to_s
|
1108
|
+
elsif trecv.is_a?(RDL::Type::MethodType)
|
1109
|
+
'Proc'
|
1015
1110
|
else
|
1016
1111
|
raise RuntimeError, "impossible to get type #{trecv}"
|
1017
1112
|
end
|
@@ -1079,27 +1174,28 @@ RUBY
|
|
1079
1174
|
end
|
1080
1175
|
|
1081
1176
|
# [+ tblock +] is the type of the block (a MethodType)
|
1082
|
-
# [+ block +] is a pair [block-args, block-body] from the block AST node
|
1177
|
+
# [+ block +] is a pair [block-args, block-body] from the block AST node OR [block-type, block-arg-AST-node]
|
1083
1178
|
# returns if the block matches type tblock
|
1084
1179
|
# otherwise throws an exception with a type error
|
1085
1180
|
def self.tc_block(scope, env, tblock, block, inst)
|
1086
|
-
# TODO more complex arg lists (same as self.typecheck?); also for
|
1087
1181
|
# TODO self is the same *except* instance_exec or instance_eval
|
1088
1182
|
raise RuntimeError, "block with block arg?" unless tblock.block.nil?
|
1089
|
-
args, body = block
|
1090
|
-
unless tblock.args.length == args.children.length
|
1091
|
-
error :arg_count_mismatch, ['block', tblock.args.length, 'block', args.children.length], (if block[0].children.empty? then block[1] else block[0] end)
|
1092
|
-
end
|
1093
1183
|
tblock = tblock.instantiate(inst)
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1184
|
+
if block[0].is_a? RDL::Type::MethodType
|
1185
|
+
error :bad_block_arg_type, [block[0], tblock], block[1] unless block[0] <= tblock
|
1186
|
+
elsif block[0].is_a?(RDL::Type::NominalType) && block[0].name == 'Proc'
|
1187
|
+
error :proc_block_arg_type, [tblock], block[1]
|
1188
|
+
else # must be [block-args, block-body]
|
1189
|
+
args, body = block
|
1190
|
+
env, targs = args_hash(scope, env, tblock, args, block, 'block')
|
1191
|
+
scope_merge(scope, outer_env: env) { |bscope|
|
1192
|
+
# note: okay if outer_env shadows, since nested scope will include outer scope by next line
|
1193
|
+
env = env.merge(Env.new(targs))
|
1194
|
+
_, body_type = if body.nil? then [nil, $__rdl_nil_type] else tc(bscope, env.merge(Env.new(targs)), body) end
|
1195
|
+
error :bad_return_type, [body_type, tblock.ret], body unless body.nil? || RDL::Type::Type.leq(body_type, tblock.ret, inst, false)
|
1196
|
+
#
|
1197
|
+
}
|
1198
|
+
end
|
1103
1199
|
end
|
1104
1200
|
|
1105
1201
|
# [+ klass +] is a string containing the class name
|
@@ -1173,7 +1269,6 @@ type_error_messages = {
|
|
1173
1269
|
masgn_bad_lhs: "no corresponding right-hand side elemnt for left-hand side assignee",
|
1174
1270
|
kw_not_allowed: "can't use `%s' in current scope",
|
1175
1271
|
kw_arg_not_allowed: "argument to `%s' not allowed in current scope",
|
1176
|
-
arg_count_mismatch: "`%s' signature expects %d arguments, actual `%s' has %d arguments",
|
1177
1272
|
no_block: "attempt to call yield in method not declared to take a block argument",
|
1178
1273
|
block_block: "can't call yield on a block expecting another block argument",
|
1179
1274
|
block_type_error: "argument type error for block\n%s",
|
@@ -1187,7 +1282,23 @@ type_error_messages = {
|
|
1187
1282
|
for_collection: "can't type for with collection of type `%s'",
|
1188
1283
|
note_type: "Type is `%s'",
|
1189
1284
|
note_message: "%s",
|
1190
|
-
recv_var_type: "
|
1285
|
+
recv_var_type: "receiver whose type is unconstrained variable `%s' not allowed",
|
1286
|
+
type_args_more: "%s type accepts more arguments than actual %s definition",
|
1287
|
+
type_args_fewer: "%s type accepts fewer arguments than actual %s definition",
|
1288
|
+
type_arg_kind_mismatch: "%s type has %s argument but actual argument is %s",
|
1289
|
+
type_args_no_kws: "%s type does not expect keyword arguments but actual expects keywords",
|
1290
|
+
type_args_no_kw: "%s type does not expect keyword argument `%s'",
|
1291
|
+
type_args_kw_mismatch: "%s type has %s keyword `%s' but actual argument is %s",
|
1292
|
+
type_args_kw_more: "%s type expects keywords `%s' that are not expected by actual %s",
|
1293
|
+
type_args_no_kw_rest: "%s type has no rest keyword but actual method accepts rest keywords",
|
1294
|
+
type_args_kw_rest: "%s type has rest keyword but actual method does not accept rest keywords",
|
1295
|
+
optional_default_type: "default value has type `%s' where type `%s' expected",
|
1296
|
+
optional_default_kw_type: "default value for `%s' has type `%s' where type `%s' expected",
|
1297
|
+
type_arg_block: "%s type does not expect block but actual %s takes block",
|
1298
|
+
bad_block_arg_type: "block argument has type `%s' but expecting type `%s'",
|
1299
|
+
non_block_block_arg: "block argument should have a block type but instead has type `%s'",
|
1300
|
+
proc_block_arg_type: "block argument is a Proc; can't tell if it matches expected type `%s'",
|
1301
|
+
no_type_for_symbol: "can't find type for method corresponding to `%s.to_proc'",
|
1191
1302
|
}
|
1192
1303
|
old_messages = Parser::MESSAGES
|
1193
1304
|
Parser.send(:remove_const, :MESSAGES)
|